diff options
Diffstat (limited to 'Http/RememberMe')
-rw-r--r-- | Http/RememberMe/AbstractRememberMeServices.php (renamed from Http/RememberMe/RememberMeServices.php) | 119 | ||||
-rw-r--r-- | Http/RememberMe/PersistentTokenBasedRememberMeServices.php | 108 | ||||
-rw-r--r-- | Http/RememberMe/RememberMeServicesInterface.php | 59 | ||||
-rw-r--r-- | Http/RememberMe/TokenBasedRememberMeServices.php | 18 |
4 files changed, 169 insertions, 135 deletions
diff --git a/Http/RememberMe/RememberMeServices.php b/Http/RememberMe/AbstractRememberMeServices.php index 4370d92..b090e24 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\User\AccountInterface; +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,15 +31,15 @@ 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 $userProviders; - protected $options; protected $logger; - protected $key; - protected $providerKey; + protected $options; + private $providerKey; + private $key; + private $userProviders; /** * Constructor @@ -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. @@ -80,7 +90,7 @@ abstract class RememberMeServices implements RememberMeServicesInterface, Logout * @param Request $request * @return TokenInterface */ - public function autoLogin(Request $request) + public final function autoLogin(Request $request) { if (null === $cookie = $request->cookies->get($this->options['name'])) { return; @@ -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,12 @@ 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); + $this->onLoginFail($request); } /** @@ -139,30 +172,26 @@ abstract class RememberMeServices implements RememberMeServicesInterface, Logout * @param TokenInterface $token The token that resulted in a successful authentication * @return void */ - public function loginSuccess(Request $request, Response $response, TokenInterface $token) + public final function loginSuccess(Request $request, Response $response, TokenInterface $token) { - if (!$token instanceof RememberMeToken) { - if (!$token->getUser() instanceof AccountInterface) { - if (null !== $this->logger) { - $this->logger->debug('Remember-me ignores token since it does not contain an AccountInterface 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 +207,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 +223,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 +231,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 +259,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 ff3306e..f2a0249 100644 --- a/Http/RememberMe/PersistentTokenBasedRememberMeServices.php +++ b/Http/RememberMe/PersistentTokenBasedRememberMeServices.php @@ -28,9 +28,9 @@ use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken; * * @author Johannes M. Schmitt <schmittjoh@gmail.com> */ -class PersistentTokenBasedRememberMeServices extends RememberMeServices +class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices { - protected $tokenProvider; + private $tokenProvider; /** * Sets the token provider @@ -46,6 +46,21 @@ class PersistentTokenBasedRememberMeServices extends RememberMeServices /** * {@inheritDoc} */ + public function logout(Request $request, Response $response, TokenInterface $token) + { + parent::logout($request, $response, $token); + + if (null !== ($cookie = $request->cookies->get($this->options['name'])) + && count($parts = $this->decodeCookie($cookie)) === 2 + ) { + list($series, $tokenValue) = $parts; + $this->tokenProvider->deleteTokenBySeries($series); + } + } + + /** + * {@inheritDoc} + */ protected function processAutoLoginCookie(array $cookieParts, Request $request) { if (count($cookieParts) !== 2) { @@ -65,11 +80,22 @@ class PersistentTokenBasedRememberMeServices extends RememberMeServices throw new AuthenticationException('The cookie has expired.'); } - $user = $this->getUserProvider($persistentToken->getClass())->loadUserByUsername($persistentToken->getUsername()); - $authenticationToken = new RememberMeToken($user, $this->providerKey, $this->key); - $authenticationToken->setPersistentToken($persistentToken); + $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 $authenticationToken; + return $this->getUserProvider($persistentToken->getClass())->loadUserByUsername($persistentToken->getUsername()); } /** @@ -77,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'], @@ -115,33 +130,6 @@ class PersistentTokenBasedRememberMeServices extends RememberMeServices } /** - * {@inheritDoc} - */ - public function logout(Request $request, Response $response, TokenInterface $token) - { - parent::logout($request, $response, $token); - - if (null !== ($cookie = $request->cookies->get($this->options['name'])) - && count($parts = $this->decodeCookie($cookie)) === 2 - ) { - list($series, $tokenValue) = $parts; - $this->tokenProvider->deleteTokenBySeries($series); - } - } - - /** - * 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 @@ -149,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); @@ -160,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 40757f4..0fd5c41 100644 --- a/Http/RememberMe/TokenBasedRememberMeServices.php +++ b/Http/RememberMe/TokenBasedRememberMeServices.php @@ -8,7 +8,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken; use Symfony\Component\Security\Core\Exception\AuthenticationException; -use Symfony\Component\Security\Core\User\AccountInterface; +use Symfony\Component\Security\Core\User\UserInterface; /* * This file is part of the Symfony package. @@ -25,7 +25,7 @@ use Symfony\Component\Security\Core\User\AccountInterface; * * @author Johannes M. Schmitt <schmittjoh@gmail.com> */ -class TokenBasedRememberMeServices extends RememberMeServices +class TokenBasedRememberMeServices extends AbstractRememberMeServices { /** * {@inheritDoc} @@ -50,8 +50,8 @@ class TokenBasedRememberMeServices extends RememberMeServices throw $ex; } - if (!$user instanceof AccountInterface) { - throw new \RuntimeException(sprintf('The UserProviderInterface implementation must return an instance of AccountInterface, but returned "%s".', get_class($user))); + if (!$user instanceof UserInterface) { + throw new \RuntimeException(sprintf('The UserProviderInterface implementation must return an instance of UserInterface, but returned "%s".', get_class($user))); } if (true !== $this->compareHashes($hash, $this->generateCookieHash($class, $username, $expires, $user->getPassword()))) { @@ -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; } /** @@ -76,7 +76,7 @@ class TokenBasedRememberMeServices extends RememberMeServices * * @return Boolean true if the two hashes are the same, false otherwise */ - protected function compareHashes($hash1, $hash2) + private function compareHashes($hash1, $hash2) { if (strlen($hash1) !== $c = strlen($hash2)) { return false; @@ -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()); } } |