diff options
-rw-r--r-- | Core/Authentication/Token/AnonymousToken.php | 8 | ||||
-rw-r--r-- | Core/Authentication/Token/PreAuthenticatedToken.php | 2 | ||||
-rw-r--r-- | Core/Authentication/Token/UsernamePasswordToken.php | 2 | ||||
-rw-r--r-- | Core/Role/Role.php | 3 | ||||
-rw-r--r-- | Core/Role/RoleInterface.php | 2 | ||||
-rw-r--r-- | Core/composer.json | 2 | ||||
-rw-r--r-- | Csrf/composer.json | 2 | ||||
-rw-r--r-- | Guard/Token/PostAuthenticationGuardToken.php | 8 | ||||
-rw-r--r-- | Guard/composer.json | 2 | ||||
-rw-r--r-- | Http/Firewall/UsernamePasswordJsonAuthenticationListener.php | 154 | ||||
-rw-r--r-- | Http/Tests/Firewall/SwitchUserListenerTest.php | 13 | ||||
-rw-r--r-- | Http/composer.json | 2 | ||||
-rw-r--r-- | Tests/Http/Firewall/UsernamePasswordJsonAuthenticationListenerTest.php | 145 | ||||
-rw-r--r-- | composer.json | 2 |
14 files changed, 324 insertions, 23 deletions
diff --git a/Core/Authentication/Token/AnonymousToken.php b/Core/Authentication/Token/AnonymousToken.php index 76c88ba..33b480c 100644 --- a/Core/Authentication/Token/AnonymousToken.php +++ b/Core/Authentication/Token/AnonymousToken.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Security\Core\Authentication\Token; -use Symfony\Component\Security\Core\Role\RoleInterface; +use Symfony\Component\Security\Core\Role\Role; /** * AnonymousToken represents an anonymous token. @@ -25,9 +25,9 @@ class AnonymousToken extends AbstractToken /** * Constructor. * - * @param string $secret A secret used to make sure the token is created by the app and not by a malicious client - * @param string|object $user The user can be a UserInterface instance, or an object implementing a __toString method or the username as a regular string - * @param RoleInterface[] $roles An array of roles + * @param string $secret A secret used to make sure the token is created by the app and not by a malicious client + * @param string|object $user The user can be a UserInterface instance, or an object implementing a __toString method or the username as a regular string + * @param Role[] $roles An array of roles */ public function __construct($secret, $user, array $roles = array()) { diff --git a/Core/Authentication/Token/PreAuthenticatedToken.php b/Core/Authentication/Token/PreAuthenticatedToken.php index a5460f5..99fb082 100644 --- a/Core/Authentication/Token/PreAuthenticatedToken.php +++ b/Core/Authentication/Token/PreAuthenticatedToken.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Security\Core\Authentication\Token; -use Symfony\Component\Security\Core\Role\RoleInterface; +use Symfony\Component\Security\Core\Role\Role; /** * PreAuthenticatedToken implements a pre-authenticated token. diff --git a/Core/Authentication/Token/UsernamePasswordToken.php b/Core/Authentication/Token/UsernamePasswordToken.php index 71d19ad..7f94677 100644 --- a/Core/Authentication/Token/UsernamePasswordToken.php +++ b/Core/Authentication/Token/UsernamePasswordToken.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Security\Core\Authentication\Token; -use Symfony\Component\Security\Core\Role\RoleInterface; +use Symfony\Component\Security\Core\Role\Role; /** * UsernamePasswordToken implements a username and password token. diff --git a/Core/Role/Role.php b/Core/Role/Role.php index 5b50981..7cb4698 100644 --- a/Core/Role/Role.php +++ b/Core/Role/Role.php @@ -12,8 +12,7 @@ namespace Symfony\Component\Security\Core\Role; /** - * Role is a simple implementation of a RoleInterface where the role is a - * string. + * Role is a simple implementation representing a role identified by a string. * * @author Fabien Potencier <fabien@symfony.com> */ diff --git a/Core/Role/RoleInterface.php b/Core/Role/RoleInterface.php index 3d4cbea..a0621ba 100644 --- a/Core/Role/RoleInterface.php +++ b/Core/Role/RoleInterface.php @@ -18,6 +18,8 @@ namespace Symfony\Component\Security\Core\Role; * supported by at least one AccessDecisionManager. * * @author Fabien Potencier <fabien@symfony.com> + * + * @deprecated The RoleInterface is deprecated since version 3.3 and will be removed in 4.0. Extend the Symfony\Component\Security\Core\Role\Role class instead. */ interface RoleInterface { diff --git a/Core/composer.json b/Core/composer.json index 25cc061..b062419 100644 --- a/Core/composer.json +++ b/Core/composer.json @@ -44,7 +44,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } } } diff --git a/Csrf/composer.json b/Csrf/composer.json index 4047fd5..913cc45 100644 --- a/Csrf/composer.json +++ b/Csrf/composer.json @@ -36,7 +36,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } } } diff --git a/Guard/Token/PostAuthenticationGuardToken.php b/Guard/Token/PostAuthenticationGuardToken.php index 5b353d9..f566b71 100644 --- a/Guard/Token/PostAuthenticationGuardToken.php +++ b/Guard/Token/PostAuthenticationGuardToken.php @@ -12,7 +12,7 @@ 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\Role\Role; use Symfony\Component\Security\Core\User\UserInterface; /** @@ -28,9 +28,9 @@ class PostAuthenticationGuardToken extends AbstractToken implements GuardTokenIn private $providerKey; /** - * @param UserInterface $user The user! - * @param string $providerKey The provider (firewall) key - * @param RoleInterface[]|string[] $roles An array of roles + * @param UserInterface $user The user! + * @param string $providerKey The provider (firewall) key + * @param (Role|string)[] $roles An array of roles * * @throws \InvalidArgumentException */ diff --git a/Guard/composer.json b/Guard/composer.json index 4980923..4bf473a 100644 --- a/Guard/composer.json +++ b/Guard/composer.json @@ -32,7 +32,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } } } diff --git a/Http/Firewall/UsernamePasswordJsonAuthenticationListener.php b/Http/Firewall/UsernamePasswordJsonAuthenticationListener.php new file mode 100644 index 0000000..bf3c621 --- /dev/null +++ b/Http/Firewall/UsernamePasswordJsonAuthenticationListener.php @@ -0,0 +1,154 @@ +<?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\Http\Firewall; + +use Psr\Log\LoggerInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\PropertyAccess\Exception\AccessException; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\Security\Core\Security; +use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; +use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; +use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; +use Symfony\Component\Security\Http\SecurityEvents; + +/** + * UsernamePasswordJsonAuthenticationListener is a stateless implementation of + * an authentication via a JSON document composed of a username and a password. + * + * @author Kévin Dunglas <dunglas@gmail.com> + */ +class UsernamePasswordJsonAuthenticationListener implements ListenerInterface +{ + private $tokenStorage; + private $authenticationManager; + private $providerKey; + private $successHandler; + private $failureHandler; + private $options; + private $logger; + private $eventDispatcher; + private $propertyAccessor; + + public function __construct(TokenStorageInterface $tokenStorage, AuthenticationManagerInterface $authenticationManager, $providerKey, AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, array $options = array(), LoggerInterface $logger = null, EventDispatcherInterface $eventDispatcher = null, PropertyAccessorInterface $propertyAccessor = null) + { + $this->tokenStorage = $tokenStorage; + $this->authenticationManager = $authenticationManager; + $this->providerKey = $providerKey; + $this->successHandler = $successHandler; + $this->failureHandler = $failureHandler; + $this->logger = $logger; + $this->eventDispatcher = $eventDispatcher; + $this->options = array_merge(array('username_path' => 'username', 'password_path' => 'password'), $options); + $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); + } + + /** + * {@inheritdoc} + */ + public function handle(GetResponseEvent $event) + { + $request = $event->getRequest(); + $data = json_decode($request->getContent()); + + if (!$data instanceof \stdClass) { + throw new BadCredentialsException('Invalid JSON.'); + } + + try { + $username = $this->propertyAccessor->getValue($data, $this->options['username_path']); + } catch (AccessException $e) { + throw new BadCredentialsException(sprintf('The key "%s" must be provided.', $this->options['username_path'])); + } + + try { + $password = $this->propertyAccessor->getValue($data, $this->options['password_path']); + } catch (AccessException $e) { + throw new BadCredentialsException(sprintf('The key "%s" must be provided.', $this->options['password_path'])); + } + + if (!is_string($username)) { + throw new BadCredentialsException(sprintf('The key "%s" must be a string.', $this->options['username_path'])); + } + + if (strlen($username) > Security::MAX_USERNAME_LENGTH) { + throw new BadCredentialsException('Invalid username.'); + } + + if (!is_string($password)) { + throw new BadCredentialsException(sprintf('The key "%s" must be a string.', $this->options['password_path'])); + } + + try { + $token = new UsernamePasswordToken($username, $password, $this->providerKey); + + $this->authenticationManager->authenticate($token); + $response = $this->onSuccess($request, $token); + } catch (AuthenticationException $e) { + $response = $this->onFailure($request, $e); + } + + $event->setResponse($response); + } + + private function onSuccess(Request $request, TokenInterface $token) + { + if (null !== $this->logger) { + $this->logger->info('User has been authenticated successfully.', array('username' => $token->getUsername())); + } + + $this->tokenStorage->setToken($token); + + if (null !== $this->eventDispatcher) { + $loginEvent = new InteractiveLoginEvent($request, $token); + $this->eventDispatcher->dispatch(SecurityEvents::INTERACTIVE_LOGIN, $loginEvent); + } + + $response = $this->successHandler->onAuthenticationSuccess($request, $token); + + if (!$response instanceof Response) { + throw new \RuntimeException('Authentication Success Handler did not return a Response.'); + } + + return $response; + } + + private function onFailure(Request $request, AuthenticationException $failed) + { + if (null !== $this->logger) { + $this->logger->info('Authentication request failed.', array('exception' => $failed)); + } + + $token = $this->tokenStorage->getToken(); + if ($token instanceof UsernamePasswordToken && $this->providerKey === $token->getProviderKey()) { + $this->tokenStorage->setToken(null); + } + + $response = $this->failureHandler->onAuthenticationFailure($request, $failed); + + if (!$response instanceof Response) { + throw new \RuntimeException('Authentication Failure Handler did not return a Response.'); + } + + return $response; + } +} diff --git a/Http/Tests/Firewall/SwitchUserListenerTest.php b/Http/Tests/Firewall/SwitchUserListenerTest.php index b80f8c6..920163a 100644 --- a/Http/Tests/Firewall/SwitchUserListenerTest.php +++ b/Http/Tests/Firewall/SwitchUserListenerTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Security\Http\Tests\Firewall; +use Symfony\Component\Security\Core\Role\Role; use Symfony\Component\Security\Http\Event\SwitchUserEvent; use Symfony\Component\Security\Http\Firewall\SwitchUserListener; use Symfony\Component\Security\Http\SecurityEvents; @@ -66,7 +67,7 @@ class SwitchUserListenerTest extends \PHPUnit_Framework_TestCase */ public function testExitUserThrowsAuthenticationExceptionIfOriginalTokenCannotBeFound() { - $token = $this->getToken(array($this->getMockBuilder('Symfony\Component\Security\Core\Role\RoleInterface')->getMock())); + $token = $this->getToken(array(new Role('the role'))); $this->tokenStorage->expects($this->any())->method('getToken')->will($this->returnValue($token)); $this->request->expects($this->any())->method('get')->with('_switch_user')->will($this->returnValue('_exit')); @@ -216,7 +217,7 @@ class SwitchUserListenerTest extends \PHPUnit_Framework_TestCase */ public function testSwitchUserIsDisallowed() { - $token = $this->getToken(array($this->getMockBuilder('Symfony\Component\Security\Core\Role\RoleInterface')->getMock())); + $token = $this->getToken(array(new Role('the role'))); $this->tokenStorage->expects($this->any())->method('getToken')->will($this->returnValue($token)); $this->request->expects($this->any())->method('get')->with('_switch_user')->will($this->returnValue('kuba')); @@ -231,8 +232,8 @@ class SwitchUserListenerTest extends \PHPUnit_Framework_TestCase public function testSwitchUser() { - $token = $this->getToken(array($this->getMockBuilder('Symfony\Component\Security\Core\Role\RoleInterface')->getMock())); - $user = $this->getMockBuilder('Symfony\Component\Security\Core\User\UserInterface')->getMock(); + $token = $this->getToken(array(new Role('the role'))); + $user = $this->getMock('Symfony\Component\Security\Core\User\UserInterface'); $user->expects($this->any())->method('getRoles')->will($this->returnValue(array())); $this->tokenStorage->expects($this->any())->method('getToken')->will($this->returnValue($token)); @@ -261,8 +262,8 @@ class SwitchUserListenerTest extends \PHPUnit_Framework_TestCase public function testSwitchUserKeepsOtherQueryStringParameters() { - $token = $this->getToken(array($this->getMockBuilder('Symfony\Component\Security\Core\Role\RoleInterface')->getMock())); - $user = $this->getMockBuilder('Symfony\Component\Security\Core\User\UserInterface')->getMock(); + $token = $this->getToken(array(new Role('the role'))); + $user = $this->getMock('Symfony\Component\Security\Core\User\UserInterface'); $user->expects($this->any())->method('getRoles')->will($this->returnValue(array())); $this->tokenStorage->expects($this->any())->method('getToken')->will($this->returnValue($token)); diff --git a/Http/composer.json b/Http/composer.json index add5d3a..87adbf0 100644 --- a/Http/composer.json +++ b/Http/composer.json @@ -43,7 +43,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } } } diff --git a/Tests/Http/Firewall/UsernamePasswordJsonAuthenticationListenerTest.php b/Tests/Http/Firewall/UsernamePasswordJsonAuthenticationListenerTest.php new file mode 100644 index 0000000..6b99a6d --- /dev/null +++ b/Tests/Http/Firewall/UsernamePasswordJsonAuthenticationListenerTest.php @@ -0,0 +1,145 @@ +<?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\Tests\Http\Firewall; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\KernelInterface; +use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\Security; +use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; +use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; +use Symfony\Component\Security\Http\Firewall\UsernamePasswordJsonAuthenticationListener; + +/** + * @author Kévin Dunglas <dunglas@gmail.com> + */ +class UsernamePasswordJsonAuthenticationListenerTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var UsernamePasswordJsonAuthenticationListener + */ + private $listener; + + private function createListener(array $options = array(), $success = true) + { + $tokenStorage = $this->getMock(TokenStorageInterface::class); + $authenticationManager = $this->getMock(AuthenticationManagerInterface::class); + + if ($success) { + $authenticationManager->method('authenticate')->willReturn(true); + } else { + $authenticationManager->method('authenticate')->willThrowException(new AuthenticationException()); + } + + $authenticationSuccessHandler = $this->getMock(AuthenticationSuccessHandlerInterface::class); + $authenticationSuccessHandler->method('onAuthenticationSuccess')->willReturn(new Response('ok')); + $authenticationFailureHandler = $this->getMock(AuthenticationFailureHandlerInterface::class); + $authenticationFailureHandler->method('onAuthenticationFailure')->willReturn(new Response('ko')); + + $this->listener = new UsernamePasswordJsonAuthenticationListener($tokenStorage, $authenticationManager, 'providerKey', $authenticationSuccessHandler, $authenticationFailureHandler, $options); + } + + public function testHandleSuccess() + { + $this->createListener(); + $request = new Request(array(), array(), array(), array(), array(), array(), '{"username": "dunglas", "password": "foo"}'); + $event = new GetResponseEvent($this->getMock(KernelInterface::class), $request, KernelInterface::MASTER_REQUEST); + + $this->listener->handle($event); + $this->assertEquals('ok', $event->getResponse()->getContent()); + } + + public function testHandleFailure() + { + $this->createListener(array(), false); + $request = new Request(array(), array(), array(), array(), array(), array(), '{"username": "dunglas", "password": "foo"}'); + $event = new GetResponseEvent($this->getMock(KernelInterface::class), $request, KernelInterface::MASTER_REQUEST); + + $this->listener->handle($event); + $this->assertEquals('ko', $event->getResponse()->getContent()); + } + + public function testUsePath() + { + $this->createListener(array('username_path' => 'user.login', 'password_path' => 'user.pwd')); + $request = new Request(array(), array(), array(), array(), array(), array(), '{"user": {"login": "dunglas", "pwd": "foo"}}'); + $event = new GetResponseEvent($this->getMock(KernelInterface::class), $request, KernelInterface::MASTER_REQUEST); + + $this->listener->handle($event); + $this->assertEquals('ok', $event->getResponse()->getContent()); + } + + /** + * @expectedException \Symfony\Component\Security\Core\Exception\BadCredentialsException + */ + public function testAttemptAuthenticationNoUsername() + { + $this->createListener(); + $request = new Request(array(), array(), array(), array(), array(), array(), '{"usr": "dunglas", "password": "foo"}'); + $event = new GetResponseEvent($this->getMock(KernelInterface::class), $request, KernelInterface::MASTER_REQUEST); + + $this->listener->handle($event); + } + + /** + * @expectedException \Symfony\Component\Security\Core\Exception\BadCredentialsException + */ + public function testAttemptAuthenticationNoPassword() + { + $this->createListener(); + $request = new Request(array(), array(), array(), array(), array(), array(), '{"username": "dunglas", "pass": "foo"}'); + $event = new GetResponseEvent($this->getMock(KernelInterface::class), $request, KernelInterface::MASTER_REQUEST); + + $this->listener->handle($event); + } + + /** + * @expectedException \Symfony\Component\Security\Core\Exception\BadCredentialsException + */ + public function testAttemptAuthenticationUsernameNotAString() + { + $this->createListener(); + $request = new Request(array(), array(), array(), array(), array(), array(), '{"username": 1, "password": "foo"}'); + $event = new GetResponseEvent($this->getMock(KernelInterface::class), $request, KernelInterface::MASTER_REQUEST); + + $this->listener->handle($event); + } + + /** + * @expectedException \Symfony\Component\Security\Core\Exception\BadCredentialsException + */ + public function testAttemptAuthenticationPasswordNotAString() + { + $this->createListener(); + $request = new Request(array(), array(), array(), array(), array(), array(), '{"username": "dunglas", "password": 1}'); + $event = new GetResponseEvent($this->getMock(KernelInterface::class), $request, KernelInterface::MASTER_REQUEST); + + $this->listener->handle($event); + } + + /** + * @expectedException \Symfony\Component\Security\Core\Exception\BadCredentialsException + */ + public function testAttemptAuthenticationUsernameTooLong() + { + $this->createListener(); + $username = str_repeat('x', Security::MAX_USERNAME_LENGTH + 1); + $request = new Request(array(), array(), array(), array(), array(), array(), sprintf('{"username": "%s", "password": 1}', $username)); + $event = new GetResponseEvent($this->getMock(KernelInterface::class), $request, KernelInterface::MASTER_REQUEST); + + $this->listener->handle($event); + } +} diff --git a/composer.json b/composer.json index 430ea54..3a63c8e 100644 --- a/composer.json +++ b/composer.json @@ -56,7 +56,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } } } |