diff options
author | Ryan Weaver <ryan@thatsquality.com> | 2015-05-17 14:39:26 -0400 |
---|---|---|
committer | Ryan Weaver <ryan@thatsquality.com> | 2015-09-20 19:24:20 -0400 |
commit | e7d6919203ec4e60e71bba1f4c84c9aee0a4c816 (patch) | |
tree | c60a9af360303ca2fac0a7eb553ecdf5dd71709f /Guard | |
parent | 85fb36d5cc968c432e0f0013d0eaab0c7c9f78c6 (diff) | |
download | symfony-security-e7d6919203ec4e60e71bba1f4c84c9aee0a4c816.zip symfony-security-e7d6919203ec4e60e71bba1f4c84c9aee0a4c816.tar.gz symfony-security-e7d6919203ec4e60e71bba1f4c84c9aee0a4c816.tar.bz2 |
Initial commit (but after some polished work) of the new Guard authentication system
Diffstat (limited to 'Guard')
-rw-r--r-- | Guard/AbstractGuardAuthenticator.php | 31 | ||||
-rw-r--r-- | Guard/Firewall/GuardAuthenticationListener.php | 180 | ||||
-rw-r--r-- | Guard/GuardAuthenticatorHandler.php | 122 | ||||
-rw-r--r-- | Guard/GuardAuthenticatorInterface.php | 119 | ||||
-rw-r--r-- | Guard/Provider/GuardAuthenticationProvider.php | 106 | ||||
-rw-r--r-- | Guard/Tests/Firewall/GuardAuthenticationListenerTest.php | 222 | ||||
-rw-r--r-- | Guard/Tests/GuardAuthenticatorHandlerTest.php | 99 | ||||
-rw-r--r-- | Guard/Tests/Provider/GuardAuthenticationProviderTest.php | 93 | ||||
-rw-r--r-- | Guard/Token/GenericGuardToken.php | 96 | ||||
-rw-r--r-- | Guard/Token/NonAuthenticatedGuardToken.php | 56 |
10 files changed, 1124 insertions, 0 deletions
diff --git a/Guard/AbstractGuardAuthenticator.php b/Guard/AbstractGuardAuthenticator.php new file mode 100644 index 0000000..ebd09bb --- /dev/null +++ b/Guard/AbstractGuardAuthenticator.php @@ -0,0 +1,31 @@ +<?php + +namespace Symfony\Component\Security\Guard; + +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Guard\Token\GenericGuardToken; + +/** + * An optional base class that creates a GenericGuardToken for you + * + * @author Ryan Weaver <weaverryan@gmail.com> + */ +abstract class AbstractGuardAuthenticator implements GuardAuthenticatorInterface +{ + /** + * Shortcut to create a GenericGuardToken for you, if you don't really + * care about which authenticated token you're using + * + * @param UserInterface $user + * @param string $providerKey + * @return GenericGuardToken + */ + public function createAuthenticatedToken(UserInterface $user, $providerKey) + { + return new GenericGuardToken( + $user, + $providerKey, + $user->getRoles() + ); + } +}
\ No newline at end of file diff --git a/Guard/Firewall/GuardAuthenticationListener.php b/Guard/Firewall/GuardAuthenticationListener.php new file mode 100644 index 0000000..a53478a --- /dev/null +++ b/Guard/Firewall/GuardAuthenticationListener.php @@ -0,0 +1,180 @@ +<?php + +namespace Symfony\Component\Security\Guard\Firewall; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\Security\Guard\GuardAuthenticatorHandler; +use Symfony\Component\Security\Guard\Token\NonAuthenticatedGuardToken; +use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; +use Symfony\Component\Security\Guard\GuardAuthenticatorInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Http\Firewall\ListenerInterface; +use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; + +/** + * Authentication listener for the "guard" system + * + * @author Ryan Weaver <weaverryan@gmail.com> + */ +class GuardAuthenticationListener implements ListenerInterface +{ + private $guardHandler; + private $authenticationManager; + private $providerKey; + private $guardAuthenticators; + private $logger; + private $rememberMeServices; + + /** + * @param GuardAuthenticatorHandler $guardHandler The Guard handler + * @param AuthenticationManagerInterface $authenticationManager An AuthenticationManagerInterface instance + * @param string $providerKey The provider (i.e. firewall) key + * @param GuardAuthenticatorInterface[] $guardAuthenticators The authenticators, with keys that match what's passed to GuardAuthenticationProvider + * @param LoggerInterface $logger A LoggerInterface instance + */ + public function __construct(GuardAuthenticatorHandler $guardHandler, AuthenticationManagerInterface $authenticationManager, $providerKey, $guardAuthenticators, LoggerInterface $logger = null) + { + if (empty($providerKey)) { + throw new \InvalidArgumentException('$providerKey must not be empty.'); + } + + $this->guardHandler = $guardHandler; + $this->authenticationManager = $authenticationManager; + $this->providerKey = $providerKey; + $this->guardAuthenticators = $guardAuthenticators; + $this->logger = $logger; + } + + /** + * Iterates over each authenticator to see if each wants to authenticate the request + * + * @param GetResponseEvent $event + */ + public function handle(GetResponseEvent $event) + { + if (null !== $this->logger) { + $this->logger->info('Checking for guard authentication credentials', array('firewall_key' => $this->providerKey, 'authenticators' => count($this->guardAuthenticators))); + } + + foreach ($this->guardAuthenticators as $key => $guardAuthenticator) { + // get a key that's unique to *this* guard authenticator + // this MUST be the same as GuardAuthenticationProvider + $uniqueGuardKey = $this->providerKey.'_'.$key; + + $this->executeGuardAuthenticator($uniqueGuardKey, $guardAuthenticator, $event); + } + } + + private function executeGuardAuthenticator($uniqueGuardKey, GuardAuthenticatorInterface $guardAuthenticator, GetResponseEvent $event) + { + $request = $event->getRequest(); + try { + if (null !== $this->logger) { + $this->logger->info('Calling getCredentialsFromRequest on guard configurator', array('firewall_key' => $this->providerKey, 'authenticator' => get_class($guardAuthenticator))); + } + + // allow the authenticator to fetch authentication info from the request + $credentials = $guardAuthenticator->getCredentialsFromRequest($request); + + // allow null to be returned to skip authentication + if (null === $credentials) { + return; + } + + // create a token with the unique key, so that the provider knows which authenticator to use + $token = new NonAuthenticatedGuardToken($credentials, $uniqueGuardKey); + + if (null !== $this->logger) { + $this->logger->info('Passing guard token information to the GuardAuthenticationProvider', array('firewall_key' => $this->providerKey, 'authenticator' => get_class($guardAuthenticator))); + } + // pass the token into the AuthenticationManager system + // this indirectly calls GuardAuthenticationProvider::authenticate() + $token = $this->authenticationManager->authenticate($token); + + if (null !== $this->logger) { + $this->logger->info('Guard authentication successful!', array('token' => $token, 'authenticator' => get_class($guardAuthenticator))); + } + + // sets the token on the token storage, etc + $this->guardHandler->authenticateWithToken($token, $request); + } catch (AuthenticationException $e) { + // oh no! Authentication failed! + + if (null !== $this->logger) { + $this->logger->info('Guard authentication failed.', array('exception' => $e, 'authenticator' => get_class($guardAuthenticator))); + } + + $response = $this->guardHandler->handleAuthenticationFailure($e, $request, $guardAuthenticator); + + if ($response instanceof Response) { + $event->setResponse($response); + } + + return; + } + + // success! + $response = $this->guardHandler->handleAuthenticationSuccess($token, $request, $guardAuthenticator, $this->providerKey); + if ($response instanceof Response) { + if (null !== $this->logger) { + $this->logger->info('Guard authenticator set success response', array('response' => $response, 'authenticator' => get_class($guardAuthenticator))); + } + + $event->setResponse($response); + } else { + if (null !== $this->logger) { + $this->logger->info('Guard authenticator set no success response: request continues', array('authenticator' => get_class($guardAuthenticator))); + } + } + + // attempt to trigger the remember me functionality + $this->triggerRememberMe($guardAuthenticator, $request, $token, $response); + } + + /** + * Should be called if this listener will support remember me. + * + * @param RememberMeServicesInterface $rememberMeServices + */ + public function setRememberMeServices(RememberMeServicesInterface $rememberMeServices) + { + $this->rememberMeServices = $rememberMeServices; + } + + /** + * Checks to see if remember me is supported in the authenticator and + * on the firewall. If it is, the RememberMeServicesInterface is notified + * + * @param GuardAuthenticatorInterface $guardAuthenticator + * @param Request $request + * @param TokenInterface $token + * @param Response $response + */ + private function triggerRememberMe(GuardAuthenticatorInterface $guardAuthenticator, Request $request, TokenInterface $token, Response $response = null) + { + if (!$guardAuthenticator->supportsRememberMe()) { + return; + } + + if (null === $this->rememberMeServices) { + if (null !== $this->logger) { + $this->logger->info('Remember me skipped: it is not configured for the firewall', array('authenticator' => get_class($guardAuthenticator))); + } + + return; + } + + if (!$response instanceof Response) { + throw new \LogicException(sprintf( + '%s::onAuthenticationSuccess *must* return a Response if you want to use the remember me functionality. Return a Response, or set remember_me to false under the guard configuration.', + get_class($guardAuthenticator) + )); + } + + $this->rememberMeServices->loginSuccess($request, $response, $token); + } +}
\ No newline at end of file diff --git a/Guard/GuardAuthenticatorHandler.php b/Guard/GuardAuthenticatorHandler.php new file mode 100644 index 0000000..e574613 --- /dev/null +++ b/Guard/GuardAuthenticatorHandler.php @@ -0,0 +1,122 @@ +<?php + +namespace Symfony\Component\Security\Guard; + +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; +use Symfony\Component\Security\Http\SecurityEvents; + +/** + * A utility class that does much of the *work* during the guard authentication process + * + * By having the logic here instead of the listener, more of the process + * can be called directly (e.g. for manual authentication) or overridden. + * + * @author Ryan Weaver <weaverryan@gmail.com> + */ +class GuardAuthenticatorHandler +{ + private $tokenStorage; + + private $dispatcher; + + public function __construct(TokenStorageInterface $tokenStorage, EventDispatcherInterface $eventDispatcher = null) + { + $this->tokenStorage = $tokenStorage; + $this->dispatcher = $eventDispatcher; + } + + /** + * Authenticates the given token in the system + * + * @param TokenInterface $token + * @param Request $request + */ + public function authenticateWithToken(TokenInterface $token, Request $request) + { + $this->tokenStorage->setToken($token); + + if (null !== $this->dispatcher) { + $loginEvent = new InteractiveLoginEvent($request, $token); + $this->dispatcher->dispatch(SecurityEvents::INTERACTIVE_LOGIN, $loginEvent); + } + } + + /** + * Returns the "on success" response for the given GuardAuthenticator + * + * @param TokenInterface $token + * @param Request $request + * @param GuardAuthenticatorInterface $guardAuthenticator + * @param string $providerKey The provider (i.e. firewall) key + * @return null|Response + */ + public function handleAuthenticationSuccess(TokenInterface $token, Request $request, GuardAuthenticatorInterface $guardAuthenticator, $providerKey) + { + $response = $guardAuthenticator->onAuthenticationSuccess($request, $token, $providerKey); + + // check that it's a Response or null + if ($response instanceof Response || null === $response) { + return $response; + } + + throw new \UnexpectedValueException(sprintf( + 'The %s::onAuthenticationSuccess method must return null or a Response object. You returned %s', + get_class($guardAuthenticator), + is_object($response) ? get_class($response) : gettype($response) + )); + } + + /** + * Convenience method for authenticating the user and returning the + * Response *if any* for success + * + * @param UserInterface $user + * @param Request $request + * @param GuardAuthenticatorInterface $authenticator + * @param string $providerKey The provider (i.e. firewall) key + * @return Response|null + */ + public function authenticateUserAndHandleSuccess(UserInterface $user, Request $request, GuardAuthenticatorInterface $authenticator, $providerKey) + { + // create an authenticated token for the User + $token = $authenticator->createAuthenticatedToken($user, $providerKey); + // authenticate this in the system + $this->authenticateWithToken($token, $request); + + // return the success metric + return $this->handleAuthenticationSuccess($token, $request, $authenticator, $providerKey); + } + + /** + * Handles an authentication failure and returns the Response for the + * GuardAuthenticator + * + * @param AuthenticationException $authenticationException + * @param Request $request + * @param GuardAuthenticatorInterface $guardAuthenticator + * @return null|Response + */ + public function handleAuthenticationFailure(AuthenticationException $authenticationException, Request $request, GuardAuthenticatorInterface $guardAuthenticator) + { + $this->tokenStorage->setToken(null); + + $response = $guardAuthenticator->onAuthenticationFailure($request, $authenticationException); + if ($response instanceof Response || null === $response) { + // returning null is ok, it means they want the request to continue + return $response; + } + + throw new \UnexpectedValueException(sprintf( + 'The %s::onAuthenticationFailure method must return null or a Response object. You returned %s', + get_class($guardAuthenticator), + is_object($response) ? get_class($response) : gettype($response) + )); + } +}
\ No newline at end of file diff --git a/Guard/GuardAuthenticatorInterface.php b/Guard/GuardAuthenticatorInterface.php new file mode 100644 index 0000000..dba8d09 --- /dev/null +++ b/Guard/GuardAuthenticatorInterface.php @@ -0,0 +1,119 @@ +<?php + +namespace Symfony\Component\Security\Guard; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; + +/** + * The interface for all "guard" authenticators + * + * The methods on this interface are called throughout the guard authentication + * process to give you the power to control most parts of the process from + * one location. + * + * @author Ryan Weaver <weaverryan@gmail.com> + */ +interface GuardAuthenticatorInterface extends AuthenticationEntryPointInterface +{ + /** + * Get the authentication credentials from the request and return them + * as any type (e.g. an associate array). If you return null, authentication + * will be skipped. + * + * Whatever value you return here will be passed to authenticate() + * + * For example, for a form login, you might: + * + * return array( + * 'username' => $request->request->get('_username'), + * 'password' => $request->request->get('_password'), + * ); + * + * Or for an API token that's on a header, you might use: + * + * return array('api_key' => $request->headers->get('X-API-TOKEN')); + * + * @param Request $request + * @return mixed|null + */ + public function getCredentialsFromRequest(Request $request); + + /** + * Return a UserInterface object based on the credentials OR throw + * an AuthenticationException + * + * The *credentials* are the return value from getCredentialsFromRequest() + * + * @param mixed $credentials + * @param UserProviderInterface $userProvider + * @throws AuthenticationException + * @return UserInterface + */ + public function authenticate($credentials, UserProviderInterface $userProvider); + + /** + * Create an authenticated token for the given user + * + * If you don't care about which token class is used or don't really + * understand what a "token" is, you can skip this method by extending + * the AbstractGuardAuthenticator class from your authenticator. + * + * @see AbstractGuardAuthenticator + * @param UserInterface $user + * @param string $providerKey The provider (i.e. firewall) key + * @return TokenInterface + */ + public function createAuthenticatedToken(UserInterface $user, $providerKey); + + /** + * Called when authentication executed, but failed (e.g. wrong username password) + * + * This should return the Response sent back to the user, like a + * RedirectResponse to the login page or a 403 response. + * + * If you return null, the request will continue, but the user will + * not be authenticated. This is probably not what you want to do. + * + * @param Request $request + * @param AuthenticationException $exception + * @return Response|null + */ + public function onAuthenticationFailure(Request $request, AuthenticationException $exception); + + /** + * Called when authentication executed and was successful! + * + * This should return the Response sent back to the user, like a + * RedirectResponse to the last page they visited. + * + * If you return null, the current request will continue, and the user + * will be authenticated. This makes sense, for example, with an API. + * + * @param Request $request + * @param TokenInterface $token + * @param string $providerKey The provider (i.e. firewall) key + * @return Response|null + */ + public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey); + + /** + * Does this method support remember me cookies? + * + * Remember me cookie will be set if *all* of the following are met: + * A) This method returns true + * B) The remember_me key under your firewall is configured + * C) The "remember me" functionality is activated. This is usually + * done by having a _remember_me checkbox in your form, but + * can be configured by the "always_remember_me" and "remember_me_parameter" + * parameters under the "remember_me" firewall key + * + * @return bool + */ + public function supportsRememberMe(); +} diff --git a/Guard/Provider/GuardAuthenticationProvider.php b/Guard/Provider/GuardAuthenticationProvider.php new file mode 100644 index 0000000..740f4c4 --- /dev/null +++ b/Guard/Provider/GuardAuthenticationProvider.php @@ -0,0 +1,106 @@ +<?php + +namespace Symfony\Component\Security\Guard\Provider; + +use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface; +use Symfony\Component\Security\Guard\GuardAuthenticatorInterface; +use Symfony\Component\Security\Guard\Token\NonAuthenticatedGuardToken; +use Symfony\Component\Security\Core\User\UserCheckerInterface; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + +/** + * Responsible for accepting the NonAuthenticatedGuardToken and calling + * the correct authenticator to retrieve the authenticated token + * + * @author Ryan Weaver <weaverryan@gmail.com> + */ +class GuardAuthenticationProvider implements AuthenticationProviderInterface +{ + /** + * @var GuardAuthenticatorInterface[] + */ + private $guardAuthenticators; + private $userProvider; + private $providerKey; + private $userChecker; + + /** + * @param GuardAuthenticatorInterface[] $guardAuthenticators The authenticators, with keys that match what's passed to GuardAuthenticationListener + * @param UserProviderInterface $userProvider The user provider + * @param string $providerKey The provider (i.e. firewall) key + * @param UserCheckerInterface $userChecker + */ + public function __construct(array $guardAuthenticators, UserProviderInterface $userProvider, $providerKey, UserCheckerInterface $userChecker) + { + $this->guardAuthenticators = $guardAuthenticators; + $this->userProvider = $userProvider; + $this->providerKey = $providerKey; + $this->userChecker = $userChecker; + } + + /** + * Finds the correct authenticator for the token and calls it + * + * @param NonAuthenticatedGuardToken $token + * @return TokenInterface + */ + public function authenticate(TokenInterface $token) + { + if (!$token instanceof NonAuthenticatedGuardToken) { + throw new \InvalidArgumentException('GuardAuthenticationProvider only supports NonAuthenticatedGuardToken'); + } + + // find the *one* GuardAuthenticator that this token originated from + foreach ($this->guardAuthenticators as $key => $guardAuthenticator) { + // get a key that's unique to *this* guard authenticator + // this MUST be the same as GuardAuthenticationListener + $uniqueGuardKey = $this->providerKey.'_'.$key; + + if ($uniqueGuardKey == $token->getGuardProviderKey()) { + return $this->authenticateViaGuard($guardAuthenticator, $token); + } + } + + throw new \LogicException(sprintf( + 'The correct GuardAuthenticator could not be found for unique key "%s". The listener and provider should be passed the same list of authenticators!?', + $token->getGuardProviderKey() + )); + } + + private function authenticateViaGuard(GuardAuthenticatorInterface $guardAuthenticator, NonAuthenticatedGuardToken $token) + { + // get the user from the GuardAuthenticator + $user = $guardAuthenticator->authenticate($token->getCredentials(), $this->userProvider); + + if (!$user instanceof UserInterface) { + throw new \UnexpectedValueException(sprintf( + 'The %s::authenticate method must return a UserInterface. You returned %s', + get_class($guardAuthenticator), + is_object($user) ? get_class($user) : gettype($user) + )); + } + + // check the AdvancedUserInterface methods! + $this->userChecker->checkPreAuth($user);; + $this->userChecker->checkPostAuth($user); + + // turn the UserInterface into a TokenInterface + $authenticatedToken = $guardAuthenticator->createAuthenticatedToken($user, $this->providerKey); + if (!$authenticatedToken instanceof TokenInterface) { + throw new \UnexpectedValueException(sprintf( + 'The %s::createAuthenticatedToken method must return a TokenInterface. You returned %s', + get_class($guardAuthenticator), + is_object($authenticatedToken) ? get_class($authenticatedToken) : gettype($authenticatedToken) + )); + } + + return $authenticatedToken; + } + + public function supports(TokenInterface $token) + { + return $token instanceof NonAuthenticatedGuardToken; + } +} diff --git a/Guard/Tests/Firewall/GuardAuthenticationListenerTest.php b/Guard/Tests/Firewall/GuardAuthenticationListenerTest.php new file mode 100644 index 0000000..b8939a9 --- /dev/null +++ b/Guard/Tests/Firewall/GuardAuthenticationListenerTest.php @@ -0,0 +1,222 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Guard\Tests\Firewall; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Guard\Firewall\GuardAuthenticationListener; +use Symfony\Component\Security\Guard\Token\NonAuthenticatedGuardToken; +use Symfony\Component\Security\Core\Exception\AuthenticationException; + +/** + * @author Ryan Weaver <weaverryan@gmail.com> + */ +class GuardAuthenticationListenerTest extends \PHPUnit_Framework_TestCase +{ + private $authenticationManager; + private $guardAuthenticatorHandler; + private $event; + private $logger; + private $request; + private $rememberMeServices; + + public function testHandleSuccess() + { + $authenticator = $this->getMock('Symfony\Component\Security\Guard\GuardAuthenticatorInterface'); + $authenticateToken = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + $providerKey = 'my_firewall'; + + $credentials = array('username' => 'weaverryan', 'password' => 'all_your_base'); + $authenticator + ->expects($this->once()) + ->method('getCredentialsFromRequest') + ->with($this->equalTo($this->request)) + ->will($this->returnValue($credentials)); + + // a clone of the token that should be created internally + $uniqueGuardKey = 'my_firewall_0'; + $nonAuthedToken = new NonAuthenticatedGuardToken($credentials, $uniqueGuardKey); + + $this->authenticationManager + ->expects($this->once()) + ->method('authenticate') + ->with($this->equalTo($nonAuthedToken)) + ->will($this->returnValue($authenticateToken)); + + $this->guardAuthenticatorHandler + ->expects($this->once()) + ->method('authenticateWithToken') + ->with($authenticateToken, $this->request); + + $this->guardAuthenticatorHandler + ->expects($this->once()) + ->method('handleAuthenticationSuccess') + ->with($authenticateToken, $this->request, $authenticator, $providerKey); + + $listener = new GuardAuthenticationListener( + $this->guardAuthenticatorHandler, + $this->authenticationManager, + $providerKey, + array($authenticator), + $this->logger + ); + + $listener->setRememberMeServices($this->rememberMeServices); + // should never be called - our handleAuthenticationSuccess() does not return a Response + $this->rememberMeServices + ->expects($this->never()) + ->method('loginSuccess'); + + $listener->handle($this->event); + } + + public function testHandleSuccessWithRememberMe() + { + $authenticator = $this->getMock('Symfony\Component\Security\Guard\GuardAuthenticatorInterface'); + $authenticateToken = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + $providerKey = 'my_firewall_with_rememberme'; + + $authenticator + ->expects($this->once()) + ->method('getCredentialsFromRequest') + ->with($this->equalTo($this->request)) + ->will($this->returnValue(array('username' => 'anything_not_empty'))); + + $this->authenticationManager + ->expects($this->once()) + ->method('authenticate') + ->will($this->returnValue($authenticateToken)); + + $successResponse = new Response('Success!'); + $this->guardAuthenticatorHandler + ->expects($this->once()) + ->method('handleAuthenticationSuccess') + ->will($this->returnValue($successResponse)); + + $listener = new GuardAuthenticationListener( + $this->guardAuthenticatorHandler, + $this->authenticationManager, + $providerKey, + array($authenticator), + $this->logger + ); + + $listener->setRememberMeServices($this->rememberMeServices); + $authenticator->expects($this->once()) + ->method('supportsRememberMe') + ->will($this->returnValue(true)); + // should be called - we do have a success Response + $this->rememberMeServices + ->expects($this->once()) + ->method('loginSuccess'); + + $listener->handle($this->event); + } + + public function testHandleCatchesAuthenticationException() + { + $authenticator = $this->getMock('Symfony\Component\Security\Guard\GuardAuthenticatorInterface'); + $providerKey = 'my_firewall2'; + + $authException = new AuthenticationException('Get outta here crazy user with a bad password!'); + $authenticator + ->expects($this->once()) + ->method('getCredentialsFromRequest') + ->will($this->throwException($authException)); + + // this is not called + $this->authenticationManager + ->expects($this->never()) + ->method('authenticate'); + + $this->guardAuthenticatorHandler + ->expects($this->once()) + ->method('handleAuthenticationFailure') + ->with($authException, $this->request, $authenticator); + + $listener = new GuardAuthenticationListener( + $this->guardAuthenticatorHandler, + $this->authenticationManager, + $providerKey, + array($authenticator), + $this->logger + ); + + $listener->handle($this->event); + } + + public function testReturnNullToSkipAuth() + { + $authenticatorA = $this->getMock('Symfony\Component\Security\Guard\GuardAuthenticatorInterface'); + $authenticatorB = $this->getMock('Symfony\Component\Security\Guard\GuardAuthenticatorInterface'); + $providerKey = 'my_firewall3'; + + $authenticatorA + ->expects($this->once()) + ->method('getCredentialsFromRequest') + ->will($this->returnValue(null)); + $authenticatorB + ->expects($this->once()) + ->method('getCredentialsFromRequest') + ->will($this->returnValue(null)); + + // this is not called + $this->authenticationManager + ->expects($this->never()) + ->method('authenticate'); + + $this->guardAuthenticatorHandler + ->expects($this->never()) + ->method('handleAuthenticationSuccess'); + + $listener = new GuardAuthenticationListener( + $this->guardAuthenticatorHandler, + $this->authenticationManager, + $providerKey, + array($authenticatorA, $authenticatorB), + $this->logger + ); + + $listener->handle($this->event); + } + + protected function setUp() + { + $this->authenticationManager = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager') + ->disableOriginalConstructor() + ->getMock(); + + $this->guardAuthenticatorHandler = $this->getMockBuilder('Symfony\Component\Security\Guard\GuardAuthenticatorHandler') + ->disableOriginalConstructor() + ->getMock(); + + $this->request = new Request(array(), array(), array(), array(), array(), array()); + + $this->event = $this->getMock('Symfony\Component\HttpKernel\Event\GetResponseEvent', array(), array(), '', false); + $this->event + ->expects($this->any()) + ->method('getRequest') + ->will($this->returnValue($this->request)); + + $this->logger = $this->getMock('Psr\Log\LoggerInterface'); + $this->rememberMeServices = $this->getMock('Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface'); + } + + protected function tearDown() + { + $this->authenticationManager = null; + $this->guardAuthenticatorHandler = null; + $this->event = null; + $this->logger = null; + $this->request = null; + } +} diff --git a/Guard/Tests/GuardAuthenticatorHandlerTest.php b/Guard/Tests/GuardAuthenticatorHandlerTest.php new file mode 100644 index 0000000..6b27e20 --- /dev/null +++ b/Guard/Tests/GuardAuthenticatorHandlerTest.php @@ -0,0 +1,99 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Guard\Tests; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Guard\GuardAuthenticatorHandler; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; +use Symfony\Component\Security\Http\SecurityEvents; + +/** + * @author Ryan Weaver <weaverryan@gmail.com> + */ +class GuardAuthenticatorHandlerTest extends \PHPUnit_Framework_TestCase +{ + private $tokenStorage; + private $dispatcher; + private $token; + private $request; + private $guardAuthenticator; + + public function testAuthenticateWithToken() + { + $this->tokenStorage->expects($this->once()) + ->method('setToken') + ->with($this->token); + + $loginEvent = new InteractiveLoginEvent($this->request, $this->token); + + $this->dispatcher + ->expects($this->once()) + ->method('dispatch') + ->with($this->equalTo(SecurityEvents::INTERACTIVE_LOGIN), $this->equalTo($loginEvent)) + ; + + $handler = new GuardAuthenticatorHandler($this->tokenStorage, $this->dispatcher); + $handler->authenticateWithToken($this->token, $this->request); + } + + public function testHandleAuthenticationSuccess() + { + $providerKey = 'my_handleable_firewall'; + $response = new Response('Guard all the things!'); + $this->guardAuthenticator->expects($this->once()) + ->method('onAuthenticationSuccess') + ->with($this->request, $this->token, $providerKey) + ->will($this->returnValue($response)); + + $handler = new GuardAuthenticatorHandler($this->tokenStorage, $this->dispatcher); + $actualResponse = $handler->handleAuthenticationSuccess($this->token, $this->request, $this->guardAuthenticator, $providerKey); + $this->assertSame($response, $actualResponse); + } + + public function testHandleAuthenticationFailure() + { + $this->tokenStorage->expects($this->once()) + ->method('setToken') + ->with(null); + $authException = new AuthenticationException('Bad password!'); + + $response = new Response('Try again, but with the right password!'); + $this->guardAuthenticator->expects($this->once()) + ->method('onAuthenticationFailure') + ->with($this->request, $authException) + ->will($this->returnValue($response)); + + $handler = new GuardAuthenticatorHandler($this->tokenStorage, $this->dispatcher); + $actualResponse = $handler->handleAuthenticationFailure($authException, $this->request, $this->guardAuthenticator); + $this->assertSame($response, $actualResponse); + } + + protected function setUp() + { + $this->tokenStorage = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'); + $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); + $this->token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + $this->request = new Request(array(), array(), array(), array(), array(), array()); + $this->guardAuthenticator = $this->getMock('Symfony\Component\Security\Guard\GuardAuthenticatorInterface'); + } + + protected function tearDown() + { + $this->tokenStorage = null; + $this->dispatcher = null; + $this->token = null; + $this->request = null; + $this->guardAuthenticator = null; + } +} diff --git a/Guard/Tests/Provider/GuardAuthenticationProviderTest.php b/Guard/Tests/Provider/GuardAuthenticationProviderTest.php new file mode 100644 index 0000000..0ef6818 --- /dev/null +++ b/Guard/Tests/Provider/GuardAuthenticationProviderTest.php @@ -0,0 +1,93 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Guard\Tests\Provider; + +use Symfony\Component\Security\Guard\Provider\GuardAuthenticationProvider; + +/** + * @author Ryan Weaver <weaverryan@gmail.com> + */ +class GuardAuthenticationProviderTest extends \PHPUnit_Framework_TestCase +{ + private $userProvider; + private $userChecker; + private $nonAuthedToken; + + public function testAuthenticate() + { + $providerKey = 'my_cool_firewall'; + + $authenticatorA = $this->getMock('Symfony\Component\Security\Guard\GuardAuthenticatorInterface'); + $authenticatorB = $this->getMock('Symfony\Component\Security\Guard\GuardAuthenticatorInterface'); + $authenticatorC = $this->getMock('Symfony\Component\Security\Guard\GuardAuthenticatorInterface'); + $authenticators = array($authenticatorA, $authenticatorB, $authenticatorC); + + // called 2 times - for authenticator A and B (stops on B because of match) + $this->nonAuthedToken->expects($this->exactly(2)) + ->method('getGuardProviderKey') + // it will return the "1" index, which will match authenticatorB + ->will($this->returnValue('my_cool_firewall_1')); + + $enteredCredentials = array( + 'username' => '_weaverryan_test_user', + 'password' => 'guard_auth_ftw', + ); + $this->nonAuthedToken->expects($this->once()) + ->method('getCredentials') + ->will($this->returnValue($enteredCredentials)); + + // authenticators A and C are never called + $authenticatorA->expects($this->never()) + ->method('authenticate'); + $authenticatorC->expects($this->never()) + ->method('authenticate'); + + $mockedUser = $this->getMock('Symfony\Component\Security\Core\User\UserInterface'); + $authenticatorB->expects($this->once()) + ->method('authenticate') + ->with($enteredCredentials, $this->userProvider) + ->will($this->returnValue($mockedUser)); + $authedToken = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + $authenticatorB->expects($this->once()) + ->method('createAuthenticatedToken') + ->with($mockedUser, $providerKey) + ->will($this->returnValue($authedToken)); + + // user checker should be called + $this->userChecker->expects($this->once()) + ->method('checkPreAuth') + ->with($mockedUser); + $this->userChecker->expects($this->once()) + ->method('checkPostAuth') + ->with($mockedUser); + + $provider = new GuardAuthenticationProvider($authenticators, $this->userProvider, $providerKey, $this->userChecker); + $actualAuthedToken = $provider->authenticate($this->nonAuthedToken); + $this->assertSame($authedToken, $actualAuthedToken); + } + + protected function setUp() + { + $this->userProvider = $this->getMock('Symfony\Component\Security\Core\User\UserProviderInterface'); + $this->userChecker = $this->getMock('Symfony\Component\Security\Core\User\UserCheckerInterface'); + $this->nonAuthedToken = $this->getMockBuilder('Symfony\Component\Security\Guard\Token\NonAuthenticatedGuardToken') + ->disableOriginalConstructor() + ->getMock(); + } + + protected function tearDown() + { + $this->userProvider = null; + $this->userChecker = null; + $this->nonAuthedToken = null; + } +} diff --git a/Guard/Token/GenericGuardToken.php b/Guard/Token/GenericGuardToken.php new file mode 100644 index 0000000..869d071 --- /dev/null +++ b/Guard/Token/GenericGuardToken.php @@ -0,0 +1,96 @@ +<?php + +namespace Symfony\Component\Security\Guard\Token; + +use Symfony\Component\Security\Core\Authentication\Token\AbstractToken; +use Symfony\Component\Security\Core\Role\RoleInterface; +use Symfony\Component\Security\Core\User\UserInterface; + +/** + * A generic token used by the AbstractGuardAuthenticator + * + * This is meant to be used as an "authenticated" token, though it + * could be set to not-authenticated later. + * + * You're free to use this (it's simple) or use any other token for + * your authenticated token + * + * @author Ryan Weaver <weaverryan@gmail.com> + */ +class GenericGuardToken extends AbstractToken +{ + private $providerKey; + + /** + * @param UserInterface $user The user! + * @param string $providerKey The provider (firewall) key + * @param RoleInterface[]|string[] $roles An array of roles + * + * @throws \InvalidArgumentException + */ + public function __construct(UserInterface $user, $providerKey, array $roles) + { + parent::__construct($roles); + + if (empty($providerKey)) { + throw new \InvalidArgumentException('$providerKey (i.e. firewall key) must not be empty.'); + } + + $this->setUser($user); + $this->providerKey = $providerKey; + + // this token is meant to be used after authentication success, so it is always authenticated + // you could set it as non authenticated later if you need to + parent::setAuthenticated(true); + } + + /** + * {@inheritdoc} + */ + public function setAuthenticated($isAuthenticated) + { + if ($isAuthenticated) { + throw new \LogicException('Cannot set this token to trusted after instantiation.'); + } + + parent::setAuthenticated(false); + } + + /** + * This is meant to be only an authenticated token, where credentials + * have already been used and are thus cleared. + * + * {@inheritdoc} + */ + public function getCredentials() + { + return array(); + } + + /** + * Returns the provider (firewall) key. + * + * @return string + */ + public function getProviderKey() + { + return $this->providerKey; + } + + /** + * {@inheritdoc} + */ + public function serialize() + { + return serialize(array($this->providerKey, parent::serialize())); + } + + /** + * {@inheritdoc} + */ + public function unserialize($serialized) + { + list($this->providerKey, $parentStr) = unserialize($serialized); + parent::unserialize($parentStr); + } +} diff --git a/Guard/Token/NonAuthenticatedGuardToken.php b/Guard/Token/NonAuthenticatedGuardToken.php new file mode 100644 index 0000000..28e21e0 --- /dev/null +++ b/Guard/Token/NonAuthenticatedGuardToken.php @@ -0,0 +1,56 @@ +<?php + +namespace Symfony\Component\Security\Guard\Token; + +use Symfony\Component\Security\Core\Authentication\Token\AbstractToken; + +/** + * The token used by the guard auth system before authentication + * + * The GuardAuthenticationListener creates this, which is then consumed + * immediately by the GuardAuthenticationProvider. If authentication is + * successful, a different authenticated token is returned + * + * @author Ryan Weaver <weaverryan@gmail.com> + */ +class NonAuthenticatedGuardToken extends AbstractToken +{ + private $credentials; + private $guardProviderKey; + + /** + * @param mixed $credentials + * @param string $guardProviderKey Unique key that bind this token to a specific GuardAuthenticatorInterface + */ + public function __construct($credentials, $guardProviderKey) + { + $this->credentials = $credentials; + $this->guardProviderKey = $guardProviderKey; + + parent::__construct(array()); + + // never authenticated + parent::setAuthenticated(false); + } + + public function getGuardProviderKey() + { + return $this->guardProviderKey; + } + + /** + * Returns the user credentials, which might be an array of anything you + * wanted to put in there (e.g. username, password, favoriteColor). + * + * @return mixed The user credentials + */ + public function getCredentials() + { + return $this->credentials; + } + + public function setAuthenticated($authenticated) + { + throw new \Exception('The NonAuthenticatedGuardToken is *always* not authenticated'); + } +} |