summaryrefslogtreecommitdiffstats
path: root/Http/RememberMe
diff options
context:
space:
mode:
authorJohannes M. Schmitt <schmittjoh@gmail.com>2011-01-26 21:34:11 +0100
committerFabien Potencier <fabien.potencier@gmail.com>2011-01-26 22:23:20 +0100
commitbebc09870cb0a7720e2c6a8c5c74585e69e8bb24 (patch)
tree0c399647cdbe504be405017e7cc04c70c53482f2 /Http/RememberMe
parentc85f3d708d2c9b00d73ca1234ccfaf50336d94b1 (diff)
downloadsymfony-security-bebc09870cb0a7720e2c6a8c5c74585e69e8bb24.zip
symfony-security-bebc09870cb0a7720e2c6a8c5c74585e69e8bb24.tar.gz
symfony-security-bebc09870cb0a7720e2c6a8c5c74585e69e8bb24.tar.bz2
namespace changes
Symfony\Component\Security -> Symfony\Component\Security\Core Symfony\Component\Security\Acl remains unchanged Symfony\Component\HttpKernel\Security -> Symfony\Component\Security\Http
Diffstat (limited to 'Http/RememberMe')
-rw-r--r--Http/RememberMe/PersistentTokenBasedRememberMeServices.php165
-rw-r--r--Http/RememberMe/RememberMeServices.php250
-rw-r--r--Http/RememberMe/RememberMeServicesInterface.php66
-rw-r--r--Http/RememberMe/TokenBasedRememberMeServices.php153
4 files changed, 634 insertions, 0 deletions
diff --git a/Http/RememberMe/PersistentTokenBasedRememberMeServices.php b/Http/RememberMe/PersistentTokenBasedRememberMeServices.php
new file mode 100644
index 0000000..73e5863
--- /dev/null
+++ b/Http/RememberMe/PersistentTokenBasedRememberMeServices.php
@@ -0,0 +1,165 @@
+<?php
+
+namespace Symfony\Component\Security\Http\RememberMe;
+
+use Symfony\Component\Security\Core\Authentication\RememberMe\TokenProviderInterface;
+use Symfony\Component\HttpFoundation\Cookie;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Security\Core\Exception\AuthenticationException;
+use Symfony\Component\Security\Core\Exception\CookieTheftException;
+use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentToken;
+use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
+use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken;
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Concrete implementation of the RememberMeServicesInterface which needs
+ * an implementation of TokenProviderInterface for providing remember-me
+ * capabilities.
+ *
+ * @author Johannes M. Schmitt <schmittjoh@gmail.com>
+ */
+class PersistentTokenBasedRememberMeServices extends RememberMeServices
+{
+ protected $tokenProvider;
+
+ /**
+ * Sets the token provider
+ *
+ * @param TokenProviderInterface $tokenProvider
+ * @return void
+ */
+ public function setTokenProvider(TokenProviderInterface $tokenProvider)
+ {
+ $this->tokenProvider = $tokenProvider;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function processAutoLoginCookie(array $cookieParts, Request $request)
+ {
+ if (count($cookieParts) !== 2) {
+ throw new AuthenticationException('The cookie is invalid.');
+ }
+
+ list($series, $tokenValue) = $cookieParts;
+ $persistentToken = $this->tokenProvider->loadTokenBySeries($series);
+
+ if ($persistentToken->getTokenValue() !== $tokenValue) {
+ $this->tokenProvider->deleteTokenBySeries($series);
+
+ throw new CookieTheftException('This token was already used. The account is possibly compromised.');
+ }
+
+ if ($persistentToken->getLastUsed()->getTimestamp() + $this->options['lifetime'] < time()) {
+ 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);
+
+ return $authenticationToken;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ 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()
+ )
+ );
+ }
+
+ $response->headers->setCookie(
+ new Cookie(
+ $this->options['name'],
+ $this->generateCookieValue($series, $tokenValue),
+ time() + $this->options['lifetime'],
+ $this->options['path'],
+ $this->options['domain'],
+ $this->options['secure'],
+ $this->options['httponly']
+ )
+ );
+ }
+
+ /**
+ * {@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
+ */
+ protected function generateRandomValue()
+ {
+ if (function_exists('openssl_random_pseudo_bytes')) {
+ $bytes = openssl_random_pseudo_bytes(32, $strong);
+
+ if (true === $strong && false !== $bytes) {
+ return base64_encode($bytes);
+ }
+ }
+
+ if (null !== $this->logger) {
+ $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));
+ }
+}
diff --git a/Http/RememberMe/RememberMeServices.php b/Http/RememberMe/RememberMeServices.php
new file mode 100644
index 0000000..8b837df
--- /dev/null
+++ b/Http/RememberMe/RememberMeServices.php
@@ -0,0 +1,250 @@
+<?php
+
+namespace Symfony\Component\Security\Http\RememberMe;
+
+use Symfony\Component\Security\Core\User\AccountInterface;
+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\User\UserProviderInterface;
+use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
+use Symfony\Component\HttpKernel\Log\LoggerInterface;
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Base class implementing the RememberMeServicesInterface
+ *
+ * @author Johannes M. Schmitt <schmittjoh@gmail.com>
+ */
+abstract class RememberMeServices implements RememberMeServicesInterface, LogoutHandlerInterface
+{
+ const COOKIE_DELIMITER = ':';
+
+ protected $userProviders;
+ protected $options;
+ protected $logger;
+ protected $key;
+ protected $providerKey;
+
+ /**
+ * Constructor
+ *
+ * @param array $userProviders
+ * @param array $options
+ * @param LoggerInterface $logger
+ */
+ public function __construct(array $userProviders, $key, $providerKey, array $options = array(), LoggerInterface $logger = null)
+ {
+ if (empty($key)) {
+ throw new \InvalidArgumentException('$key must not be empty.');
+ }
+ if (empty($providerKey)) {
+ throw new \InvalidArgumentException('$providerKey must not be empty.');
+ }
+ if (0 === count($userProviders)) {
+ throw new \InvalidArgumentException('You must provide at least one user provider.');
+ }
+
+ $this->userProviders = $userProviders;
+ $this->key = $key;
+ $this->providerKey = $providerKey;
+ $this->options = $options;
+ $this->logger = $logger;
+ }
+
+ /**
+ * Implementation of RememberMeServicesInterface. Detects whether a remember-me
+ * cookie was set, decodes it, and hands it to subclasses for further processing.
+ *
+ * @param Request $request
+ * @return TokenInterface
+ */
+ public function autoLogin(Request $request)
+ {
+ if (null === $cookie = $request->cookies->get($this->options['name'])) {
+ return;
+ }
+
+ if (null !== $this->logger) {
+ $this->logger->debug('Remember-me cookie detected.');
+ }
+
+ $cookieParts = $this->decodeCookie($cookie);
+ $token = $this->processAutoLoginCookie($cookieParts, $request);
+
+ if (!$token instanceof TokenInterface) {
+ throw new \RuntimeException('processAutoLoginCookie() must return a TokenInterface implementation.');
+ }
+
+ if (null !== $this->logger) {
+ $this->logger->debug('Remember-me cookie accepted.');
+ }
+
+ return $token;
+ }
+
+ /**
+ * Implementation for LogoutHandlerInterface. Deletes the cookie.
+ *
+ * @param Request $request
+ * @param Response $response
+ * @param TokenInterface $token
+ * @return void
+ */
+ public function logout(Request $request, Response $response, TokenInterface $token)
+ {
+ $this->cancelCookie($response);
+ }
+
+ /**
+ * Implementation for RememberMeServicesInterface. Deletes the cookie when
+ * an attempted authentication fails.
+ *
+ * @param Request $request
+ * @param Response $response
+ * @return void
+ */
+ public function loginFail(Request $request, Response $response)
+ {
+ $this->cancelCookie($response);
+ }
+
+ /**
+ * Implementation for RememberMeServicesInterface. This is called when an
+ * authentication is successful.
+ *
+ * @param Request $request
+ * @param Response $response
+ * @param TokenInterface $token The token that resulted in a successful authentication
+ * @return void
+ */
+ public 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 (!$this->isRememberMeRequested($request)) {
+ if (null !== $this->logger) {
+ $this->logger->debug('Remember-me was not requested.');
+ }
+
+ return;
+ }
+
+ if (null !== $this->logger) {
+ $this->logger->debug('Remember-me was requested; setting cookie.');
+ }
+ } else if (null !== $this->logger) {
+ $this->logger->debug('Re-newing remember-me token; setting cookie.');
+ }
+
+ $this->onLoginSuccess($request, $response, $token);
+ }
+
+ /**
+ * Subclasses should validate the cookie and do any additional processing
+ * that is required. This is called from autoLogin().
+ *
+ * @param array $cookieParts
+ * @param Request $request
+ * @return TokenInterface
+ */
+ abstract protected function processAutoLoginCookie(array $cookieParts, Request $request);
+
+ /**
+ * This is called after a user has been logged in successfully, and has
+ * requested remember-me capabilities. The implementation usually sets a
+ * cookie and possibly stores a persistent record of it.
+ *
+ * @param Request $request
+ * @param Response $response
+ * @param TokenInterface $token
+ * @return void
+ */
+ abstract protected function onLoginSuccess(Request $request, Response $response, TokenInterface $token);
+
+ protected function getUserProvider($class)
+ {
+ foreach ($this->userProviders as $provider) {
+ if ($provider->supportsClass($class)) {
+ return $provider;
+ }
+ }
+
+ throw new \RuntimeException(sprintf('There is no user provider that supports class "%s".', $class));
+ }
+
+ /**
+ * Decodes the raw cookie value
+ *
+ * @param string $rawCookie
+ * @return array
+ */
+ protected function decodeCookie($rawCookie)
+ {
+ return explode(self::COOKIE_DELIMITER, base64_decode($rawCookie));
+ }
+
+ /**
+ * Encodes the cookie parts
+ *
+ * @param array $cookieParts
+ * @return string
+ */
+ protected function encodeCookie(array $cookieParts)
+ {
+ return base64_encode(implode(self::COOKIE_DELIMITER, $cookieParts));
+ }
+
+ /**
+ * Deletes the remember-me cookie
+ *
+ * @param Response $response
+ * @return void
+ */
+ protected function cancelCookie(Response $response)
+ {
+ 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']);
+ }
+
+ /**
+ * Checks whether remember-me capabilities where requested
+ *
+ * @param Request $request
+ * @return Boolean
+ */
+ protected function isRememberMeRequested(Request $request)
+ {
+ if (true === $this->options['always_remember_me']) {
+ return true;
+ }
+
+ $parameter = $request->request->get($this->options['remember_me_parameter']);
+
+ if ($parameter === null && null !== $this->logger) {
+ $this->logger->debug(sprintf('Did not send remember-me cookie (remember-me parameter "%s" was not sent).', $this->options['remember_me_parameter']));
+ }
+
+ return $parameter === 'true' || $parameter === 'on' || $parameter === '1' || $parameter === 'yes';
+ }
+}
diff --git a/Http/RememberMe/RememberMeServicesInterface.php b/Http/RememberMe/RememberMeServicesInterface.php
new file mode 100644
index 0000000..0fcf99d
--- /dev/null
+++ b/Http/RememberMe/RememberMeServicesInterface.php
@@ -0,0 +1,66 @@
+<?php
+namespace Symfony\Component\Security\Http\RememberMe;
+
+use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpFoundation\Request;
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * 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.
+ *
+ * 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.
+ *
+ * If a browser has presented an unauthorised cookie for whatever reason,
+ * 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.
+ *
+ * This method needs to take care of invalidating the cookie.
+ */
+ function loginFail(Request $request, Response $response);
+
+ /**
+ * 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
+ * (such as a HTTP POST parameter) that indicates the browser has explicitly
+ * requested for the authentication to be remembered.
+ */
+ 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
new file mode 100644
index 0000000..da5479d
--- /dev/null
+++ b/Http/RememberMe/TokenBasedRememberMeServices.php
@@ -0,0 +1,153 @@
+<?php
+
+namespace Symfony\Component\Security\Http\RememberMe;
+
+use Symfony\Component\HttpFoundation\Cookie;
+use Symfony\Component\HttpFoundation\Request;
+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;
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Concrete implementation of the RememberMeServicesInterface providing
+ * remember-me capabilities without requiring a TokenProvider.
+ *
+ * @author Johannes M. Schmitt <schmittjoh@gmail.com>
+ */
+class TokenBasedRememberMeServices extends RememberMeServices
+{
+ /**
+ * {@inheritDoc}
+ */
+ protected function processAutoLoginCookie(array $cookieParts, Request $request)
+ {
+ if (count($cookieParts) !== 4) {
+ throw new AuthenticationException('The cookie is invalid.');
+ }
+
+ list($class, $username, $expires, $hash) = $cookieParts;
+ if (false === $username = base64_decode($username, true)) {
+ throw new AuthenticationException('$username contains a character from outside the base64 alphabet.');
+ }
+ try {
+ $user = $this->getUserProvider($class)->loadUserByUsername($username);
+ } catch (\Exception $ex) {
+ if (!$ex instanceof AuthenticationException) {
+ $ex = new AuthenticationException($ex->getMessage(), null, $ex->getCode(), $ex);
+ }
+
+ 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 (true !== $this->compareHashes($hash, $this->generateCookieHash($class, $username, $expires, $user->getPassword()))) {
+ throw new AuthenticationException('The cookie\'s hash is invalid.');
+ }
+
+ if ($expires < time()) {
+ throw new AuthenticationException('The cookie has expired.');
+ }
+
+ return new RememberMeToken($user, $this->providerKey, $this->key);
+ }
+
+ /**
+ * Compares two hashes using a constant-time algorithm to avoid (remote)
+ * timing attacks.
+ *
+ * This is the same implementation as used in the BasePasswordEncoder.
+ *
+ * @param string $hash1 The first hash
+ * @param string $hash2 The second hash
+ *
+ * @return Boolean true if the two hashes are the same, false otherwise
+ */
+ protected function compareHashes($hash1, $hash2)
+ {
+ if (strlen($hash1) !== $c = strlen($hash2)) {
+ return false;
+ }
+
+ $result = 0;
+ for ($i = 0; $i < $c; $i++) {
+ $result |= ord($hash1[$i]) ^ ord($hash2[$i]);
+ }
+
+ return 0 === $result;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ 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());
+
+ $response->headers->setCookie(
+ new Cookie(
+ $this->options['name'],
+ $value,
+ $expires,
+ $this->options['path'],
+ $this->options['domain'],
+ $this->options['secure'],
+ $this->options['httponly']
+ )
+ );
+ }
+
+ /**
+ * Generates the cookie value
+ *
+ * @param strign $class
+ * @param string $username The username
+ * @param integer $expires The unixtime when the cookie expires
+ * @param string $password The encoded password
+ * @throws \RuntimeException if username contains invalid chars
+ * @return string
+ */
+ protected function generateCookieValue($class, $username, $expires, $password)
+ {
+ return $this->encodeCookie(array(
+ $class,
+ base64_encode($username),
+ $expires,
+ $this->generateCookieHash($class, $username, $expires, $password)
+ ));
+ }
+
+ /**
+ * Generates a hash for the cookie to ensure it is not being tempered with
+ *
+ * @param string $class
+ * @param string $username The username
+ * @param integer $expires The unixtime when the cookie expires
+ * @param string $password The encoded password
+ * @throws \RuntimeException when the private key is empty
+ * @return string
+ */
+ protected function generateCookieHash($class, $username, $expires, $password)
+ {
+ return hash('sha256', $class.$username.$expires.$password.$this->key);
+ }
+}