diff options
-rw-r--r-- | Core/Authentication/AuthenticationManagerInterface.php | 2 | ||||
-rw-r--r-- | Core/Authentication/Token/RememberMeToken.php | 10 | ||||
-rw-r--r-- | Http/Firewall/RememberMeListener.php | 77 | ||||
-rw-r--r-- | Http/RememberMe/AbstractRememberMeServices.php (renamed from Http/RememberMe/RememberMeServices.php) | 110 | ||||
-rw-r--r-- | Http/RememberMe/PersistentTokenBasedRememberMeServices.php | 74 | ||||
-rw-r--r-- | Http/RememberMe/RememberMeServicesInterface.php | 59 | ||||
-rw-r--r-- | Http/RememberMe/TokenBasedRememberMeServices.php | 10 |
7 files changed, 164 insertions, 178 deletions
diff --git a/Core/Authentication/AuthenticationManagerInterface.php b/Core/Authentication/AuthenticationManagerInterface.php index 5f407f2..36cdc92 100644 --- a/Core/Authentication/AuthenticationManagerInterface.php +++ b/Core/Authentication/AuthenticationManagerInterface.php @@ -27,7 +27,7 @@ interface AuthenticationManagerInterface * * @param TokenInterface $token The TokenInterface instance to authenticate * - * @return TokenInterface An authenticated TokenInterface instance + * @return TokenInterface An authenticated TokenInterface instance, never null * * @throws AuthenticationException if the authentication fails */ diff --git a/Core/Authentication/Token/RememberMeToken.php b/Core/Authentication/Token/RememberMeToken.php index 038198a..7978427 100644 --- a/Core/Authentication/Token/RememberMeToken.php +++ b/Core/Authentication/Token/RememberMeToken.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Security\Core\Authentication\Token; -use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentTokenInterface; use Symfony\Component\Security\Core\User\UserInterface; /** @@ -23,7 +22,6 @@ class RememberMeToken extends AbstractToken { private $key; private $providerKey; - private $persistentToken; /** * Constructor. @@ -32,7 +30,7 @@ class RememberMeToken extends AbstractToken * @param string $providerKey * @param string $key */ - public function __construct(UserInterface $user, $providerKey, $key, PersistentTokenInterface $persistentToken = null) { + public function __construct(UserInterface $user, $providerKey, $key) { parent::__construct($user->getRoles()); if (empty($key)) { @@ -45,7 +43,6 @@ class RememberMeToken extends AbstractToken $this->providerKey = $providerKey; $this->key = $key; - $this->persistentToken = $persistentToken; $this->setUser($user); parent::setAuthenticated(true); @@ -70,11 +67,6 @@ class RememberMeToken extends AbstractToken return $this->key; } - public function getPersistentToken() - { - return $this->persistentToken; - } - public function getCredentials() { return ''; diff --git a/Http/Firewall/RememberMeListener.php b/Http/Firewall/RememberMeListener.php index 6b23679..f0755db 100644 --- a/Http/Firewall/RememberMeListener.php +++ b/Http/Firewall/RememberMeListener.php @@ -35,7 +35,6 @@ class RememberMeListener implements ListenerInterface private $rememberMeServices; private $authenticationManager; private $logger; - private $lastState; private $eventDispatcher; /** @@ -63,7 +62,6 @@ class RememberMeListener implements ListenerInterface public function register(EventDispatcherInterface $dispatcher) { $dispatcher->connect('core.security', array($this, 'checkCookies'), 0); - $dispatcher->connect('core.response', array($this, 'updateCookies'), 0); $this->eventDispatcher = $dispatcher; } @@ -73,7 +71,6 @@ class RememberMeListener implements ListenerInterface */ public function unregister(EventDispatcherInterface $dispatcher) { - $dispatcher->disconnect('core.response', array($this, 'updateCookies')); } /** @@ -83,74 +80,36 @@ class RememberMeListener implements ListenerInterface */ public function checkCookies(EventInterface $event) { - $this->lastState = null; - if (null !== $this->securityContext->getToken()) { return; } - try { - if (null === $token = $this->rememberMeServices->autoLogin($event->get('request'))) { - return; - } - - try { - if (null === $token = $this->authenticationManager->authenticate($token)) { - return; - } - - $this->securityContext->setToken($token); - - if (null !== $this->eventDispatcher) { - $this->eventDispatcher->notify(new Event($this, 'security.interactive_login', array('request' => $event->get('request'), 'token' => $token))); - } - - if (null !== $this->logger) { - $this->logger->debug('SecurityContext populated with remember-me token.'); - } + $request = $event->get('request'); + if (null === $token = $this->rememberMeServices->autoLogin($request)) { + return; + } - $this->lastState = $token; - } catch (AuthenticationException $failed) { - if (null !== $this->logger) { - $this->logger->debug( - 'SecurityContext not populated with remember-me token as the' - .' AuthenticationManager rejected the AuthenticationToken returned' - .' by the RememberMeServices: '.$failed->getMessage() - ); - } + try { + $token = $this->authenticationManager->authenticate($token); + $this->securityContext->setToken($token); - $this->lastState = $failed; + if (null !== $this->eventDispatcher) { + $this->eventDispatcher->notify(new Event($this, 'security.interactive_login', array('request' => $request, 'token' => $token))); } - } catch (AuthenticationException $cookieInvalid) { - $this->lastState = $cookieInvalid; if (null !== $this->logger) { - $this->logger->debug('The presented cookie was invalid: '.$cookieInvalid->getMessage()); + $this->logger->debug('SecurityContext populated with remember-me token.'); } - - // silently ignore everything except a cookie theft exception - if ($cookieInvalid instanceof CookieTheftException) { - throw $cookieInvalid; + } catch (AuthenticationException $failed) { + if (null !== $this->logger) { + $this->logger->debug( + 'SecurityContext not populated with remember-me token as the' + .' AuthenticationManager rejected the AuthenticationToken returned' + .' by the RememberMeServices: '.$failed->getMessage() + ); } - } - } - - /** - * Update cookies - * @param Event $event - */ - public function updateCookies(EventInterface $event, Response $response) - { - if (HttpKernelInterface::MASTER_REQUEST !== $event->get('request_type')) { - return $response; - } - if ($this->lastState instanceof TokenInterface) { - $this->rememberMeServices->loginSuccess($event->get('request'), $response, $this->lastState); - } else if ($this->lastState instanceof AuthenticationException) { - $this->rememberMeServices->loginFail($event->get('request'), $response); + $this->rememberMeServices->loginFail($request); } - - return $response; } }
\ No newline at end of file diff --git a/Http/RememberMe/RememberMeServices.php b/Http/RememberMe/AbstractRememberMeServices.php index e0ed52b..daf68f5 100644 --- a/Http/RememberMe/RememberMeServices.php +++ b/Http/RememberMe/AbstractRememberMeServices.php @@ -2,14 +2,19 @@ namespace Symfony\Component\Security\Http\RememberMe; +use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken; use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\Exception\UnsupportedUserException; +use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\Security\Core\Exception\CookieTheftException; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpKernel\Log\LoggerInterface; /* @@ -26,14 +31,14 @@ use Symfony\Component\HttpKernel\Log\LoggerInterface; * * @author Johannes M. Schmitt <schmittjoh@gmail.com> */ -abstract class RememberMeServices implements RememberMeServicesInterface, LogoutHandlerInterface +abstract class AbstractRememberMeServices implements RememberMeServicesInterface, LogoutHandlerInterface { const COOKIE_DELIMITER = ':'; - protected $options; protected $logger; - protected $providerKey; - protected $key; + protected $options; + private $providerKey; + private $key; private $userProviders; /** @@ -73,6 +78,11 @@ abstract class RememberMeServices implements RememberMeServicesInterface, Logout return $this->options['remember_me_parameter']; } + public function getKey() + { + return $this->key; + } + /** * Implementation of RememberMeServicesInterface. Detects whether a remember-me * cookie was set, decodes it, and hands it to subclasses for further processing. @@ -91,17 +101,40 @@ abstract class RememberMeServices implements RememberMeServicesInterface, Logout } $cookieParts = $this->decodeCookie($cookie); - $token = $this->processAutoLoginCookie($cookieParts, $request); - if (!$token instanceof TokenInterface) { - throw new \RuntimeException('processAutoLoginCookie() must return a TokenInterface implementation.'); - } + try { + $user = $this->processAutoLoginCookie($cookieParts, $request); - if (null !== $this->logger) { - $this->logger->debug('Remember-me cookie accepted.'); + if (!$user instanceof UserInterface) { + throw new \RuntimeException('processAutoLoginCookie() must return a UserInterface implementation.'); + } + + if (null !== $this->logger) { + $this->logger->debug('Remember-me cookie accepted.'); + } + + return new RememberMeToken($user, $this->providerKey, $this->key); + } catch (CookieTheftException $theft) { + $this->cancelCookie($request); + + throw $theft; + } catch (UsernameNotFoundException $notFound) { + if (null !== $this->logger) { + $this->logger->debug('User for remember-me cookie not found.'); + } + } catch (UnsupportedUserException $unSupported) { + if (null !== $this->logger) { + $this->logger->debug('User class for remember-me cookie not supported.'); + } + } catch (AuthenticationException $invalid) { + if (null !== $this->logger) { + $this->logger->debug('Remember-Me authentication failed: '.$invalid->getMessage()); + } } - return $token; + $this->cancelCookie($request); + + return null; } /** @@ -114,7 +147,7 @@ abstract class RememberMeServices implements RememberMeServicesInterface, Logout */ public function logout(Request $request, Response $response, TokenInterface $token) { - $this->cancelCookie($response); + $this->cancelCookie($request); } /** @@ -122,12 +155,11 @@ abstract class RememberMeServices implements RememberMeServicesInterface, Logout * an attempted authentication fails. * * @param Request $request - * @param Response $response * @return void */ - public function loginFail(Request $request, Response $response) + public final function loginFail(Request $request) { - $this->cancelCookie($response); + $this->cancelCookie($request); } /** @@ -141,28 +173,24 @@ abstract class RememberMeServices implements RememberMeServicesInterface, Logout */ public final function loginSuccess(Request $request, Response $response, TokenInterface $token) { - if (!$token instanceof RememberMeToken) { - if (!$token->getUser() instanceof UserInterface) { - if (null !== $this->logger) { - $this->logger->debug('Remember-me ignores token since it does not contain an UserInterface implementation.'); - } - - return; + if (!$token->getUser() instanceof UserInterface) { + if (null !== $this->logger) { + $this->logger->debug('Remember-me ignores token since it does not contain an UserInterface implementation.'); } - if (!$this->isRememberMeRequested($request)) { - if (null !== $this->logger) { - $this->logger->debug('Remember-me was not requested.'); - } - - return; - } + return; + } + if (!$this->isRememberMeRequested($request)) { if (null !== $this->logger) { - $this->logger->debug('Remember-me was requested; setting cookie.'); + $this->logger->debug('Remember-me was not requested.'); } - } else if (null !== $this->logger) { - $this->logger->debug('Re-newing remember-me token; setting cookie.'); + + return; + } + + if (null !== $this->logger) { + $this->logger->debug('Remember-me was requested; setting cookie.'); } $this->onLoginSuccess($request, $response, $token); @@ -178,6 +206,10 @@ abstract class RememberMeServices implements RememberMeServicesInterface, Logout */ abstract protected function processAutoLoginCookie(array $cookieParts, Request $request); + protected function onLoginFail(Request $request) + { + } + /** * This is called after a user has been logged in successfully, and has * requested remember-me capabilities. The implementation usually sets a @@ -190,7 +222,7 @@ abstract class RememberMeServices implements RememberMeServicesInterface, Logout */ abstract protected function onLoginSuccess(Request $request, Response $response, TokenInterface $token); - protected function getUserProvider($class) + protected final function getUserProvider($class) { foreach ($this->userProviders as $provider) { if ($provider->supportsClass($class)) { @@ -198,7 +230,7 @@ abstract class RememberMeServices implements RememberMeServicesInterface, Logout } } - throw new \RuntimeException(sprintf('There is no user provider that supports class "%s".', $class)); + throw new UnsupportedUserException(sprintf('There is no user provider that supports class "%s".', $class)); } /** @@ -226,16 +258,16 @@ abstract class RememberMeServices implements RememberMeServicesInterface, Logout /** * Deletes the remember-me cookie * - * @param Response $response + * @param Request $request * @return void */ - protected function cancelCookie(Response $response) + protected function cancelCookie(Request $request) { if (null !== $this->logger) { $this->logger->debug(sprintf('Clearing remember-me cookie "%s"', $this->options['name'])); } - $response->headers->clearCookie($this->options['name'], $this->options['path'], $this->options['domain']); + $request->attributes->set(self::COOKIE_ATTR_NAME, new Cookie($this->options['name'], null, 1, $this->options['path'], $this->options['domain'])); } /** diff --git a/Http/RememberMe/PersistentTokenBasedRememberMeServices.php b/Http/RememberMe/PersistentTokenBasedRememberMeServices.php index 351ad03..f2a0249 100644 --- a/Http/RememberMe/PersistentTokenBasedRememberMeServices.php +++ b/Http/RememberMe/PersistentTokenBasedRememberMeServices.php @@ -28,7 +28,7 @@ use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken; * * @author Johannes M. Schmitt <schmittjoh@gmail.com> */ -class PersistentTokenBasedRememberMeServices extends RememberMeServices +class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices { private $tokenProvider; @@ -80,9 +80,22 @@ class PersistentTokenBasedRememberMeServices extends RememberMeServices throw new AuthenticationException('The cookie has expired.'); } - $user = $this->getUserProvider($persistentToken->getClass())->loadUserByUsername($persistentToken->getUsername()); + $series = $persistentToken->getSeries(); + $tokenValue = $this->generateRandomValue(); + $this->tokenProvider->updateToken($series, $tokenValue, new \DateTime()); + $request->attributes->set(self::COOKIE_ATTR_NAME, + new Cookie( + $this->options['name'], + $this->encodeCookie(array($series, $tokenValue)), + time() + $this->options['lifetime'], + $this->options['path'], + $this->options['domain'], + $this->options['secure'], + $this->options['httponly'] + ) + ); - return new RememberMeToken($user, $this->providerKey, $this->key, $persistentToken); + return $this->getUserProvider($persistentToken->getClass())->loadUserByUsername($persistentToken->getUsername()); } /** @@ -90,34 +103,23 @@ class PersistentTokenBasedRememberMeServices extends RememberMeServices */ protected function onLoginSuccess(Request $request, Response $response, TokenInterface $token) { - if ($token instanceof RememberMeToken) { - if (null === $persistentToken = $token->getPersistentToken()) { - throw new \RuntimeException('RememberMeToken must contain a PersistentTokenInterface implementation when used as login.'); - } - - $series = $persistentToken->getSeries(); - $tokenValue = $this->generateRandomValue(); - - $this->tokenProvider->updateToken($series, $tokenValue, new \DateTime()); - } else { - $series = $this->generateRandomValue(); - $tokenValue = $this->generateRandomValue(); - - $this->tokenProvider->createNewToken( - new PersistentToken( - get_class($user = $token->getUser()), - $user->getUsername(), - $series, - $tokenValue, - new \DateTime() - ) - ); - } + $series = $this->generateRandomValue(); + $tokenValue = $this->generateRandomValue(); + + $this->tokenProvider->createNewToken( + new PersistentToken( + get_class($user = $token->getUser()), + $user->getUsername(), + $series, + $tokenValue, + new \DateTime() + ) + ); $response->headers->setCookie( new Cookie( $this->options['name'], - $this->generateCookieValue($series, $tokenValue), + $this->encodeCookie(array($series, $tokenValue)), time() + $this->options['lifetime'], $this->options['path'], $this->options['domain'], @@ -128,18 +130,6 @@ class PersistentTokenBasedRememberMeServices extends RememberMeServices } /** - * Generates the value for the cookie - * - * @param string $series - * @param string $tokenValue - * @return string - */ - protected function generateCookieValue($series, $tokenValue) - { - return $this->encodeCookie(array($series, $tokenValue)); - } - - /** * Generates a cryptographically strong random value * * @return string @@ -147,7 +137,7 @@ class PersistentTokenBasedRememberMeServices extends RememberMeServices protected function generateRandomValue() { if (function_exists('openssl_random_pseudo_bytes')) { - $bytes = openssl_random_pseudo_bytes(32, $strong); + $bytes = openssl_random_pseudo_bytes(64, $strong); if (true === $strong && false !== $bytes) { return base64_encode($bytes); @@ -158,6 +148,6 @@ class PersistentTokenBasedRememberMeServices extends RememberMeServices $this->logger->warn('Could not produce a cryptographically strong random value. Please install/update the OpenSSL extension.'); } - return base64_encode(hash('sha256', uniqid(mt_rand(), true), true)); + return base64_encode(hash('sha512', uniqid(mt_rand(), true), true)); } -} +}
\ No newline at end of file diff --git a/Http/RememberMe/RememberMeServicesInterface.php b/Http/RememberMe/RememberMeServicesInterface.php index b038a0d..c740d28 100644 --- a/Http/RememberMe/RememberMeServicesInterface.php +++ b/Http/RememberMe/RememberMeServicesInterface.php @@ -17,50 +17,67 @@ use Symfony\Component\HttpFoundation\Request; /** * Interface that needs to be implemented by classes which provide remember-me * capabilities. - * + * * We provide two implementations out-of-the-box: * - TokenBasedRememberMeServices (does not require a TokenProvider) * - PersistentTokenBasedRememberMeServices (requires a TokenProvider) - * + * * @author Johannes M. Schmitt <schmittjoh@gmail.com> */ interface RememberMeServicesInterface { /** - * This method will be called whenever the SecurityContext does not contain - * an TokenInterface object and the framework wishes to provide an implementation - * with an opportunity to authenticate the request using remember-me capabilities. - * + * This attribute name can be used by the implementation if it needs to set + * a cookie on the Request when there is no actual Response, yet. + * + * @var string + */ + const COOKIE_ATTR_NAME = '_security_remember_me_cookie'; + + /** + * This method will be called whenever the SecurityContext does not contain + * an TokenInterface object and the framework wishes to provide an implementation + * with an opportunity to authenticate the request using remember-me capabilities. + * * No attempt whatsoever is made to determine whether the browser has requested * remember-me services or presented a valid cookie. Any and all such determinations - * are left to the implementation of this method. - * + * are left to the implementation of this method. + * * If a browser has presented an unauthorised cookie for whatever reason, - * make sure to throw an AuthenticationException as this will consequentially + * make sure to throw an AuthenticationException as this will consequentially * result in a call to loginFail() and therefore an invalidation of the cookie. - * + * * @param Request $request * @return TokenInterface */ function autoLogin(Request $request); - + /** - * Called whenever an authentication attempt was made, but the credentials - * supplied by the user were missing or otherwise invalid. - * + * Called whenever an interactive authentication attempt was made, but the + * credentials supplied by the user were missing or otherwise invalid. + * * This method needs to take care of invalidating the cookie. + * + * @param Request $request + * @return void */ - function loginFail(Request $request, Response $response); + function loginFail(Request $request); /** - * Called whenever authentication attempt is successful (e.g. a form login). - * - * An implementation may always set a remember-me cookie in the Response, - * although this is not recommended. - * - * Instead, implementations should typically look for a request parameter + * Called whenever an interactive authentication attempt is successful + * (e.g. a form login). + * + * An implementation may always set a remember-me cookie in the Response, + * although this is not recommended. + * + * Instead, implementations should typically look for a request parameter * (such as a HTTP POST parameter) that indicates the browser has explicitly * requested for the authentication to be remembered. + * + * @param Request $request + * @param Response $response + * @param TokenInterface $token + * @return void */ function loginSuccess(Request $request, Response $response, TokenInterface $token); }
\ No newline at end of file diff --git a/Http/RememberMe/TokenBasedRememberMeServices.php b/Http/RememberMe/TokenBasedRememberMeServices.php index 206e10b..0fd5c41 100644 --- a/Http/RememberMe/TokenBasedRememberMeServices.php +++ b/Http/RememberMe/TokenBasedRememberMeServices.php @@ -25,7 +25,7 @@ use Symfony\Component\Security\Core\User\UserInterface; * * @author Johannes M. Schmitt <schmittjoh@gmail.com> */ -class TokenBasedRememberMeServices extends RememberMeServices +class TokenBasedRememberMeServices extends AbstractRememberMeServices { /** * {@inheritDoc} @@ -62,7 +62,7 @@ class TokenBasedRememberMeServices extends RememberMeServices throw new AuthenticationException('The cookie has expired.'); } - return new RememberMeToken($user, $this->providerKey, $this->key); + return $user; } /** @@ -95,10 +95,6 @@ class TokenBasedRememberMeServices extends RememberMeServices */ protected function onLoginSuccess(Request $request, Response $response, TokenInterface $token) { - if ($token instanceof RememberMeToken) { - return; - } - $user = $token->getUser(); $expires = time() + $this->options['lifetime']; $value = $this->generateCookieValue(get_class($user), $user->getUsername(), $expires, $user->getPassword()); @@ -150,6 +146,6 @@ class TokenBasedRememberMeServices extends RememberMeServices */ protected function generateCookieHash($class, $username, $expires, $password) { - return hash('sha256', $class.$username.$expires.$password.$this->key); + return hash('sha256', $class.$username.$expires.$password.$this->getKey()); } } |