-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Security] Idle sessions expiration. #12807
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
@@ -28,6 +28,8 @@ | |||
|
|||
<parameter key="security.authentication.switchuser_listener.class">Symfony\Component\Security\Http\Firewall\SwitchUserListener</parameter> | |||
|
|||
<parameter key="security.authentication.sessionexpiration_listener.class">Symfony\Component\Security\Http\Firewall\SessionExpirationListener</parameter> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better don't use a parameter for the class name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not? All other firewall listeners clases are defined using a parameter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because such class names are deprecated and will be removed in 3.0, so we don't introduce new ones.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, didn't know!
0c81e2d
to
b69255a
Compare
I've just rebased 2.7 branch to get green tests again. With components enviroment option the tests fail trying to autoload new classes, not included with the component installation. Is there anything I can do here? |
namespace Symfony\Bundle\SecurityBundle\Tests\Functional; | ||
|
||
/** | ||
* @author Antonio J. García Lagar <aj@garcialagar.es> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we generally don't put @author
tags on test files
@stof Thanks for your review. I think all errors you found were fixed. |
Insert a reference to PR symfony/symfony#12807
Insert a reference to PR symfony/symfony#12807
@@ -258,6 +258,15 @@ | |||
<argument type="service" id="event_dispatcher" on-invalid="null"/> | |||
</service> | |||
|
|||
<service id="security.authentication.sessionexpiration_listener" class="Symfony\Component\Security\Http\Firewall\SessionExpirationListener" public="false" abstract="true"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would call it session_expiration_listener
(separating all words with underscores, not only half of them)
e596c70
to
ab2b434
Compare
ping @symfony/deciders What about this feature? Is it something we want in core? |
anonymous: ~ | ||
logout: | ||
target: /second/target | ||
path: /second/logout |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do we need this firewall for?
Use `Psr\Log\LoggerInterface` instead of the deprecated `Symfony\Component\HttpKernel\Log\LoggerInterface`
036ed33
to
1b18bdd
Compare
1b18bdd
to
c877aad
Compare
53eb071
to
eae647e
Compare
What's the advantage of this over using I just want to make sure I understand the need for this. :) |
The Another problem with Another advantage of this PR is that when an expired session is detected, you can control how your app should react, catching the You can read a more detailed explanation in https://stackoverflow.com/questions/520237/how-do-i-expire-a-php-session-after-30-minutes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a sound PR imo. And so, I'd like to see it merged :). I just sent you some feedback @ajgarlag. Sorry for the long delays - sometimes security stuff (due to complexity, sensitivity) is one that is the most difficult to review. So, thanks for your patience. I only see one potential blocker, and it's about the logout handlers inside the listener.
{ | ||
return 'FormLoginBundle'; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we just use the FormLoginBundle
directly in the tests? Then we could remove this file and the new after_login.html.twig
. That bundle already gives us a functional /profile
URL, which is all we really need I think.
$client->request('GET', '/protected_resource'); | ||
$this->assertEquals(200, $client->getResponse()->getStatusCode()); | ||
|
||
sleep(5); //Wait for session to expire |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// Wait
(add a space)
->canBeUnset() | ||
->children() | ||
->integerNode('max_idle_time')->defaultValue(ini_get('session.gc_maxlifetime'))->min(1)->end() | ||
->scalarNode('expiration_url')->defaultNull()->end() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about expiration_path
? This seems more consistent with the keys under form_login
, for example. And this could also be a URL, correct? It looks like you set it up that way :)
@@ -17,7 +17,7 @@ | |||
], | |||
"require": { | |||
"php": ">=5.5.9", | |||
"symfony/security": "~3.1,>=3.1.2", | |||
"symfony/security": "~3.2", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can be removed - on the 3.4
branch, this is already updated to "symfony/security": "~3.4|~4.0",
} | ||
|
||
if (null !== $this->logger) { | ||
$this->logger->info(sprintf("Expired session detected for user named '%s'", $token->getUsername())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit pick, but use '
around the string and then "%s"
inside. We almost always wrap strings in single quotes (sometimes this isn't followed, but usually when we need "\n"
)
} | ||
|
||
$this->tokenStorage->setToken(null); | ||
$session->invalidate(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, so this is the trickiest part of this PR. You're effectively imitating what LogoutListener
does + some of its logout handlers (e.g. SessionLogoutHandler
). So then (to play devil's advocate), why not actually inject the logout handlers and use them here? We are logging the user out... so shouldn't those handlers be called? And to go further, couldn't we use the normal DefaultLogoutSuccessHandler
to figure out where to take the user next?
The disadvantage to this is that you couldn't do something different based on a normal user logout versus a session expiration logout. But, is that a problem? And if it is, could we dispatch a special event (e.g. security.session_expired
) in this class, before calling the logout handlers?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After 3 years, my vision is slightly different. I'll remove the $tokenStorage->setToken(null)
so this listener will only detect the session expiration and optionally (enabled by default) will invalidate the session.
For me, the logout is an interactive user action and different from the session expiration case.
If you need to force the logout you could disable the session invalidation and redirect the user to the logout path. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm... so then the purpose of this listener would be to only invalidate the session after a certain amount of idle time. If that's the case... then this is kind of unrelated to security, right? This could just be a general framework feature of the session: a framework-level way to invalidate sessions that are idle. Wdyt?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@weaverryan It took me some time to review this, as the history behind this PR is too old. It is the result of an split of #12009, which was a reimplementation of #786, into #12807(this PR) and #12810, so I decided to write a recap
We have to implement 2 different things here: first, a feature to detect idle sessions, and last, to provide a default behavior once the expired session is detected.
Detecting idle sessions
It is now implemented as a firewall listener, so it can be enabled or disabled for different firewalls in the same app, but if the session is invalidated inside one firewall, it will logout from any firewall that uses the ContextListener
to restore the token.
If we decide that this feature is not related to security, but to the framework, we can implement it as a kernel.request
event listener that, IMO, should be executed with higher priority than the firewall one, so the user is not authenticated from an expired session. I'm not sure which component (HttpKernel?) should provide this listener.
Anyway, once the expired session is detected, I think that a new event should be dispatched (currently not implemented). Your proposed name security.session_expired
it's only an option if we decide that this feature is a security concern.
Default behavior on expired sessions
For me, the best option here is to invalidate the session (should we allow not to override this?), and redirect the user to a given path that will return an accurate response depending on the given request (html, json, etc...) or to throw a SessionExpiredException
in no path is defined. If we implement this feature as a kernel event listener, this behavior should be hooked to the new session_expired
event.
In a previous comment you proposed to force an user logout calling the LogoutSuccessHandler
and all defined LogoutHandlers
, but I see two problems here:
- In the app there could be more than one firewall, so; which logout should be executed when an expired session is detected if we decide that this option should not be implemented as a firewall listener, but as a kernel listener?
- If we decide to execute the logout from the current firewall, (in this case the expired session listener should be a firewall listener, or has to be executed after the firewall listener), what should be done if there is no logout listener defined for the current path?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @ajgarlag!
Sorry for the slow reply again :p.
After reading your reply, it seems to me that this PR indeed has nothing to do with security. We're simply implementing a framework-level "session timeout" feature. This is necessary because there's no reliable way to do this with php.ini configuration. If there were, then we would just have the user do that.
With that in mind, this 100% seems like "session" configuration. And so, it would be configured under the framework.session
configuration. And I don't think ANY other configuration is necessary - no need to have expiration_url
. Nope, we would simply invalidate the user's session and that's it (e.g. allow the request to continue).
IF we want to allow the user to hook into this further, then dispatching the event we discussed may make sense. I WOULD do that, then we can see what others think.
To summarize (and tell me if you disagree):
-
This is not related to security at all - implement it under the
framework.session
configuration. The listener would be configured in FrameworkBundle, but the listener/subscriber class itself would live in the HttpKernel component. -
When a session "expires", simply invalidate the session and do nothing else. Well, let's also dispatch a new event, and we can see if people think it's a good idea.
I'm chiming in a bit randomly so my comment might be totally wrong. |
Looks like @nicolas-grekas is right. Closing. |
This new firewall allows you to block long running sessions.
When the session cookie is valid until the browser is closed (
cookie_lifetime = 0
), but the user left the browser open for a long time, the session will be invalidated if themax_idle_time
is exceeded. Themax_idle_time
value should be lesser or equal than the one fromsession.gc_maxlifetime
.If an
expiration_url
is set, the user agent will be redirected to this URL after the session invalidation. If this parameter is not set, aSessionExpiredException
will be thrown.