diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php index 962c68eb2b..2edfb3ff34 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory; use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; @@ -30,6 +31,7 @@ class FormLoginFactory extends AbstractFactory implements AuthenticatorFactoryIn $this->addOption('password_parameter', '_password'); $this->addOption('csrf_parameter', '_csrf_token'); $this->addOption('csrf_token_id', 'authenticate'); + $this->addOption('enable_csrf', false); $this->addOption('post_only', true); } @@ -61,6 +63,10 @@ class FormLoginFactory extends AbstractFactory implements AuthenticatorFactoryIn protected function createAuthProvider(ContainerBuilder $container, string $id, array $config, string $userProviderId) { + if ($config['enable_csrf'] ?? false) { + throw new InvalidConfigurationException('The "enable_csrf" option of "form_login" is only available when "security.enable_authenticator_manager" is set to "true", use "csrf_token_generator" instead.'); + } + $provider = 'security.authentication.provider.dao.'.$id; $container ->setDefinition($provider, new ChildDefinition('security.authentication.provider.dao')) @@ -99,6 +105,10 @@ class FormLoginFactory extends AbstractFactory implements AuthenticatorFactoryIn public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId): string { + if (isset($config['csrf_token_generator'])) { + throw new InvalidConfigurationException('The "csrf_token_generator" option of "form_login" is only available when "security.enable_authenticator_manager" is set to "false", use "enable_csrf" instead.'); + } + $authenticatorId = 'security.authenticator.form_login.'.$id; $options = array_intersect_key($config, $this->options); $container diff --git a/src/Symfony/Component/Security/CHANGELOG.md b/src/Symfony/Component/Security/CHANGELOG.md index da0d2cb8aa..adf023eac3 100644 --- a/src/Symfony/Component/Security/CHANGELOG.md +++ b/src/Symfony/Component/Security/CHANGELOG.md @@ -9,6 +9,7 @@ CHANGELOG * Hash the persistent RememberMe token value in database. * Added `LogoutEvent` to allow custom logout listeners. * Deprecated `LogoutSuccessHandlerInterface` and `LogoutHandlerInterface` in favor of listening on the `LogoutEvent`. + * Added experimental new security using `Http\Authenticator\AuthenticatorInterface`, `Http\Authentication\AuthenticatorManager` and `Http\Firewall\AuthenticatorManagerListener`. 5.0.0 ----- diff --git a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php index 381195d833..36a9916105 100644 --- a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php +++ b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php @@ -19,11 +19,13 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\AuthenticationEvents; use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent; use Symfony\Component\Security\Core\Exception\AuthenticationException; -use Symfony\Component\Security\Core\Exception\BadCredentialsException; -use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\AnonymousPassport; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; use Symfony\Component\Security\Http\Event\LoginFailureEvent; use Symfony\Component\Security\Http\Event\LoginSuccessEvent; @@ -60,13 +62,16 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent $this->eraseCredentials = $eraseCredentials; } - public function authenticateUser(UserInterface $user, AuthenticatorInterface $authenticator, Request $request): ?Response + /** + * @param BadgeInterface[] $badges Optionally, pass some Passport badges to use for the manual login + */ + public function authenticateUser(UserInterface $user, AuthenticatorInterface $authenticator, Request $request, array $badges = []): ?Response { // create an authenticated token for the User - $token = $authenticator->createAuthenticatedToken($user, $this->providerKey); + $token = $authenticator->createAuthenticatedToken($passport = new SelfValidatingPassport($user, $badges), $this->providerKey); // authenticate this in the system - return $this->handleAuthenticationSuccess($token, $request, $authenticator); + return $this->handleAuthenticationSuccess($token, $passport, $request, $authenticator); } public function supports(Request $request): ?bool @@ -133,7 +138,7 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent continue; } - $response = $this->executeAuthenticator($key, $authenticator, $request); + $response = $this->executeAuthenticator($authenticator, $request); if (null !== $response) { if (null !== $this->logger) { $this->logger->debug('The "{authenticator}" authenticator set the response. Any later authenticator will not be called', ['authenticator' => \get_class($authenticator)]); @@ -146,29 +151,35 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent return null; } - private function executeAuthenticator(string $uniqueAuthenticatorKey, AuthenticatorInterface $authenticator, Request $request): ?Response + private function executeAuthenticator(AuthenticatorInterface $authenticator, Request $request): ?Response { try { - if (null !== $this->logger) { - $this->logger->debug('Calling getCredentials() on authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($authenticator)]); + // get the passport from the Authenticator + $passport = $authenticator->authenticate($request); + + // check the passport (e.g. password checking) + $event = new VerifyAuthenticatorCredentialsEvent($authenticator, $passport); + $this->eventDispatcher->dispatch($event); + + // check if all badges are resolved + $passport->checkIfCompletelyResolved(); + + // create the authenticated token + $authenticatedToken = $authenticator->createAuthenticatedToken($passport, $this->providerKey); + if (true === $this->eraseCredentials) { + $authenticatedToken->eraseCredentials(); } - // allow the authenticator to fetch authentication info from the request - $credentials = $authenticator->getCredentials($request); - - if (null === $credentials) { - throw new \UnexpectedValueException(sprintf('The return value of "%1$s::getCredentials()" must not be null. Return false from "%1$s::supports()" instead.', \get_class($authenticator))); + if (null !== $this->eventDispatcher) { + $this->eventDispatcher->dispatch(new AuthenticationSuccessEvent($authenticatedToken), AuthenticationEvents::AUTHENTICATION_SUCCESS); } - // authenticate the credentials (e.g. check password) - $token = $this->authenticateViaAuthenticator($authenticator, $credentials); - if (null !== $this->logger) { - $this->logger->info('Authenticator successful!', ['token' => $token, 'authenticator' => \get_class($authenticator)]); + $this->logger->info('Authenticator successful!', ['token' => $authenticatedToken, 'authenticator' => \get_class($authenticator)]); } // success! (sets the token on the token storage, etc) - $response = $this->handleAuthenticationSuccess($token, $request, $authenticator); + $response = $this->handleAuthenticationSuccess($authenticatedToken, $passport, $request, $authenticator); if ($response instanceof Response) { return $response; } @@ -189,35 +200,7 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent } } - private function authenticateViaAuthenticator(AuthenticatorInterface $authenticator, $credentials): TokenInterface - { - // get the user from the Authenticator - $user = $authenticator->getUser($credentials); - if (null === $user) { - throw new UsernameNotFoundException(sprintf('Null returned from "%s::getUser()".', \get_class($authenticator))); - } - - $event = new VerifyAuthenticatorCredentialsEvent($authenticator, $credentials, $user); - $this->eventDispatcher->dispatch($event); - if (true !== $event->areCredentialsValid()) { - throw new BadCredentialsException(sprintf('Authentication failed because "%s" did not approve the credentials.', \get_class($authenticator))); - } - - // turn the UserInterface into a TokenInterface - $authenticatedToken = $authenticator->createAuthenticatedToken($user, $this->providerKey); - - if (true === $this->eraseCredentials) { - $authenticatedToken->eraseCredentials(); - } - - if (null !== $this->eventDispatcher) { - $this->eventDispatcher->dispatch(new AuthenticationSuccessEvent($authenticatedToken), AuthenticationEvents::AUTHENTICATION_SUCCESS); - } - - return $authenticatedToken; - } - - private function handleAuthenticationSuccess(TokenInterface $authenticatedToken, Request $request, AuthenticatorInterface $authenticator): ?Response + private function handleAuthenticationSuccess(TokenInterface $authenticatedToken, PassportInterface $passport, Request $request, AuthenticatorInterface $authenticator): ?Response { $this->tokenStorage->setToken($authenticatedToken); @@ -227,7 +210,11 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent $this->eventDispatcher->dispatch($loginEvent, SecurityEvents::INTERACTIVE_LOGIN); } - $this->eventDispatcher->dispatch($loginSuccessEvent = new LoginSuccessEvent($authenticator, $authenticatedToken, $request, $response, $this->providerKey)); + if ($passport instanceof AnonymousPassport) { + return $response; + } + + $this->eventDispatcher->dispatch($loginSuccessEvent = new LoginSuccessEvent($authenticator, $passport, $authenticatedToken, $request, $response, $this->firewallName)); return $loginSuccessEvent->getResponse(); } diff --git a/src/Symfony/Component/Security/Http/Authenticator/AbstractAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AbstractAuthenticator.php index 3683827d12..51a49a3b17 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AbstractAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AbstractAuthenticator.php @@ -12,7 +12,9 @@ namespace Symfony\Component\Security\Http\Authenticator; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\Exception\LogicException; +use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\UserPassportInterface; use Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken; /** @@ -30,8 +32,12 @@ abstract class AbstractAuthenticator implements AuthenticatorInterface * * @return PostAuthenticationToken */ - public function createAuthenticatedToken(UserInterface $user, string $providerKey): TokenInterface + public function createAuthenticatedToken(PassportInterface $passport, string $providerKey): TokenInterface { - return new PostAuthenticationToken($user, $providerKey, $user->getRoles()); + if (!$passport instanceof UserPassportInterface) { + throw new LogicException(sprintf('Passport does not contain a user, overwrite "createAuthenticatedToken()" in "%s" to create a custom authenticated token.', \get_class($this))); + } + + return new PostAuthenticationToken($passport->getUser(), $providerKey, $passport->getUser()->getRoles()); } } diff --git a/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php index 69ded7b062..f45fb3d074 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php @@ -25,7 +25,7 @@ use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface * * @experimental in 5.1 */ -abstract class AbstractLoginFormAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface, RememberMeAuthenticatorInterface, InteractiveAuthenticatorInterface +abstract class AbstractLoginFormAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface, InteractiveAuthenticatorInterface { /** * Return the URL to the login page. @@ -57,11 +57,6 @@ abstract class AbstractLoginFormAuthenticator extends AbstractAuthenticator impl return new RedirectResponse($url); } - public function supportsRememberMe(): bool - { - return true; - } - public function isInteractive(): bool { return true; diff --git a/src/Symfony/Component/Security/Http/Authenticator/AbstractPreAuthenticatedAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AbstractPreAuthenticatedAuthenticator.php index b3a02bf1bd..435de68e98 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AbstractPreAuthenticatedAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AbstractPreAuthenticatedAuthenticator.php @@ -19,8 +19,10 @@ use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInt use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; -use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PreAuthenticatedUserBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; /** * The base authenticator for authenticators to use pre-authenticated @@ -32,7 +34,7 @@ use Symfony\Component\Security\Core\User\UserProviderInterface; * @internal * @experimental in Symfony 5.1 */ -abstract class AbstractPreAuthenticatedAuthenticator implements InteractiveAuthenticatorInterface, CustomAuthenticatedInterface +abstract class AbstractPreAuthenticatedAuthenticator implements InteractiveAuthenticatorInterface { private $userProvider; private $tokenStorage; @@ -63,7 +65,7 @@ abstract class AbstractPreAuthenticatedAuthenticator implements InteractiveAuthe $this->clearToken($e); if (null !== $this->logger) { - $this->logger->debug('Skipping pre-authenticated authenticator as a BadCredentialsException is thrown.', ['exception' => $e, 'authenticator' => \get_class($this)]); + $this->logger->debug('Skipping pre-authenticated authenticator as a BadCredentialsException is thrown.', ['exception' => $e, 'authenticator' => static::class]); } return false; @@ -71,7 +73,7 @@ abstract class AbstractPreAuthenticatedAuthenticator implements InteractiveAuthe if (null === $username) { if (null !== $this->logger) { - $this->logger->debug('Skipping pre-authenticated authenticator no username could be extracted.', ['authenticator' => \get_class($this)]); + $this->logger->debug('Skipping pre-authenticated authenticator no username could be extracted.', ['authenticator' => static::class]); } return false; @@ -82,27 +84,17 @@ abstract class AbstractPreAuthenticatedAuthenticator implements InteractiveAuthe return true; } - public function getCredentials(Request $request) + public function authenticate(Request $request): PassportInterface { - return [ - 'username' => $request->attributes->get('_pre_authenticated_username'), - ]; + $username = $request->attributes->get('_pre_authenticated_username'); + $user = $this->userProvider->loadUserByUsername($username); + + return new SelfValidatingPassport($user, [new PreAuthenticatedUserBadge()]); } - public function getUser($credentials): ?UserInterface + public function createAuthenticatedToken(PassportInterface $passport, string $providerKey): TokenInterface { - return $this->userProvider->loadUserByUsername($credentials['username']); - } - - public function checkCredentials($credentials, UserInterface $user): bool - { - // the user is already authenticated before it entered Symfony - return true; - } - - public function createAuthenticatedToken(UserInterface $user, string $providerKey): TokenInterface - { - return new PreAuthenticatedToken($user, null, $providerKey); + return new PreAuthenticatedToken($passport->getUser(), null, $providerKey, $passport->getUser()->getRoles()); } public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response diff --git a/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php index 4b6214668c..27a315b0f5 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php @@ -17,8 +17,8 @@ use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken; 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\User; -use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\AnonymousPassport; +use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; /** * @author Wouter de Jong @@ -27,7 +27,7 @@ use Symfony\Component\Security\Core\User\UserInterface; * @final * @experimental in 5.1 */ -class AnonymousAuthenticator implements AuthenticatorInterface, CustomAuthenticatedInterface +class AnonymousAuthenticator implements AuthenticatorInterface { private $secret; private $tokenStorage; @@ -45,23 +45,12 @@ class AnonymousAuthenticator implements AuthenticatorInterface, CustomAuthentica return null === $this->tokenStorage->getToken() ? null : false; } - public function getCredentials(Request $request) + public function authenticate(Request $request): PassportInterface { - return []; + return new AnonymousPassport(); } - public function checkCredentials($credentials, UserInterface $user): bool - { - // anonymous users do not have credentials - return true; - } - - public function getUser($credentials): ?UserInterface - { - return new User('anon.', null); - } - - public function createAuthenticatedToken(UserInterface $user, string $providerKey): TokenInterface + public function createAuthenticatedToken(PassportInterface $passport, string $providerKey): TokenInterface { return new AnonymousToken($this->secret, 'anon.', []); } diff --git a/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php b/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php index 0f1053e109..d80356e713 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php @@ -15,7 +15,7 @@ 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\Http\Authenticator\Passport\PassportInterface; /** * The interface for all authenticators. @@ -38,39 +38,19 @@ interface AuthenticatorInterface public function supports(Request $request): ?bool; /** - * Get the authentication credentials from the request and return them - * as any type (e.g. an associate array). + * Create a passport for the current request. * - * Whatever value you return here will be passed to getUser() and checkCredentials() + * The passport contains the user, credentials and any additional information + * that has to be checked by the Symfony Security system. For example, a login + * form authenticator will probably return a passport containing the user, the + * presented password and the CSRF token value. * - * For example, for a form login, you might: - * - * return [ - * 'username' => $request->request->get('_username'), - * 'password' => $request->request->get('_password'), - * ]; - * - * Or for an API token that's on a header, you might use: - * - * return ['api_key' => $request->headers->get('X-API-TOKEN')]; - * - * @return mixed Any non-null value - * - * @throws \UnexpectedValueException If null is returned - */ - public function getCredentials(Request $request); - - /** - * Return a UserInterface object based on the credentials. - * - * You may throw an AuthenticationException if you wish. If you return - * null, then a UsernameNotFoundException is thrown for you. - * - * @param mixed $credentials the value returned from getCredentials() + * You may throw any AuthenticationException in this method in case of error (e.g. + * a UsernameNotFoundException when the user cannot be found). * * @throws AuthenticationException */ - public function getUser($credentials): ?UserInterface; + public function authenticate(Request $request): PassportInterface; /** * Create an authenticated token for the given user. @@ -80,19 +60,10 @@ interface AuthenticatorInterface * the AbstractAuthenticator class from your authenticator. * * @see AbstractAuthenticator - */ - public function createAuthenticatedToken(UserInterface $user, string $providerKey): TokenInterface; - - /** - * 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 PassportInterface $passport The passport returned from authenticate() */ - public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response; + public function createAuthenticatedToken(PassportInterface $passport, string $providerKey): TokenInterface; /** * Called when authentication executed and was successful! @@ -104,4 +75,15 @@ interface AuthenticatorInterface * will be authenticated. This makes sense, for example, with an API. */ public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response; + + /** + * 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. + */ + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response; } diff --git a/src/Symfony/Component/Security/Http/Authenticator/CsrfProtectedAuthenticatorInterface.php b/src/Symfony/Component/Security/Http/Authenticator/CsrfProtectedAuthenticatorInterface.php deleted file mode 100644 index 0f93ad1e86..0000000000 --- a/src/Symfony/Component/Security/Http/Authenticator/CsrfProtectedAuthenticatorInterface.php +++ /dev/null @@ -1,34 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Security\Http\Authenticator; - -/** - * This interface can be implemented to automatically add CSF - * protection to the authenticator. - * - * @author Wouter de Jong - */ -interface CsrfProtectedAuthenticatorInterface -{ - /** - * An arbitrary string used to generate the value of the CSRF token. - * Using a different string for each authenticator improves its security. - */ - public function getCsrfTokenId(): string; - - /** - * Returns the CSRF token contained in credentials if any. - * - * @param mixed $credentials the credentials returned by getCredentials() - */ - public function getCsrfToken($credentials): ?string; -} diff --git a/src/Symfony/Component/Security/Http/Authenticator/CustomAuthenticatedInterface.php b/src/Symfony/Component/Security/Http/Authenticator/CustomAuthenticatedInterface.php deleted file mode 100644 index 79b995e55f..0000000000 --- a/src/Symfony/Component/Security/Http/Authenticator/CustomAuthenticatedInterface.php +++ /dev/null @@ -1,36 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Security\Http\Authenticator; - -use Symfony\Component\Security\Core\Exception\AuthenticationException; -use Symfony\Component\Security\Core\User\UserInterface; - -/** - * This interface should be implemented by authenticators that - * require custom (not password related) authentication. - * - * @author Wouter de Jong - */ -interface CustomAuthenticatedInterface -{ - /** - * Returns true if the credentials are valid. - * - * If false is returned, authentication will fail. You may also throw - * an AuthenticationException if you wish to cause authentication to fail. - * - * @param mixed $credentials the value returned from getCredentials() - * - * @throws AuthenticationException - */ - public function checkCredentials($credentials, UserInterface $user): bool; -} diff --git a/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php index 5aaf96437f..0bbbb6eb83 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php @@ -17,12 +17,20 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; 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\AuthenticationServiceException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Security; +use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; +use Symfony\Component\Security\Http\Authenticator\Passport\Passport; +use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; use Symfony\Component\Security\Http\HttpUtils; use Symfony\Component\Security\Http\ParameterBagUtils; @@ -33,7 +41,7 @@ use Symfony\Component\Security\Http\ParameterBagUtils; * @final * @experimental in 5.1 */ -class FormLoginAuthenticator extends AbstractLoginFormAuthenticator implements PasswordAuthenticatedInterface, CsrfProtectedAuthenticatorInterface +class FormLoginAuthenticator extends AbstractLoginFormAuthenticator { private $httpUtils; private $userProvider; @@ -52,7 +60,7 @@ class FormLoginAuthenticator extends AbstractLoginFormAuthenticator implements P 'password_parameter' => '_password', 'check_path' => '/login_check', 'post_only' => true, - + 'enable_csrf' => false, 'csrf_parameter' => '_csrf_token', 'csrf_token_id' => 'authenticate', ], $options); @@ -69,17 +77,55 @@ class FormLoginAuthenticator extends AbstractLoginFormAuthenticator implements P && $this->httpUtils->checkRequestPath($request, $this->options['check_path']); } - public function getCredentials(Request $request): array + public function authenticate(Request $request): PassportInterface + { + $credentials = $this->getCredentials($request); + $user = $this->userProvider->loadUserByUsername($credentials['username']); + if (!$user instanceof UserInterface) { + throw new AuthenticationServiceException('The user provider must return a UserInterface object.'); + } + + $passport = new Passport($user, new PasswordCredentials($credentials['password']), [new RememberMeBadge()]); + if ($this->options['enable_csrf']) { + $passport->addBadge(new CsrfTokenBadge($this->options['csrf_token_id'], $credentials['csrf_token'])); + } + + if ($this->userProvider instanceof PasswordUpgraderInterface) { + $passport->addBadge(new PasswordUpgradeBadge($credentials['password'], $this->userProvider)); + } + + return $passport; + } + + /** + * @param Passport $passport + */ + public function createAuthenticatedToken(PassportInterface $passport, $providerKey): TokenInterface + { + return new UsernamePasswordToken($passport->getUser(), null, $providerKey, $passport->getUser()->getRoles()); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response + { + return $this->successHandler->onAuthenticationSuccess($request, $token); + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response + { + return $this->failureHandler->onAuthenticationFailure($request, $exception); + } + + private function getCredentials(Request $request): array { $credentials = []; $credentials['csrf_token'] = ParameterBagUtils::getRequestParameterValue($request, $this->options['csrf_parameter']); if ($this->options['post_only']) { $credentials['username'] = ParameterBagUtils::getParameterBagValue($request->request, $this->options['username_parameter']); - $credentials['password'] = ParameterBagUtils::getParameterBagValue($request->request, $this->options['password_parameter']); + $credentials['password'] = ParameterBagUtils::getParameterBagValue($request->request, $this->options['password_parameter']) ?? ''; } else { $credentials['username'] = ParameterBagUtils::getRequestParameterValue($request, $this->options['username_parameter']); - $credentials['password'] = ParameterBagUtils::getRequestParameterValue($request, $this->options['password_parameter']); + $credentials['password'] = ParameterBagUtils::getRequestParameterValue($request, $this->options['password_parameter']) ?? ''; } if (!\is_string($credentials['username']) && (!\is_object($credentials['username']) || !method_exists($credentials['username'], '__toString'))) { @@ -96,39 +142,4 @@ class FormLoginAuthenticator extends AbstractLoginFormAuthenticator implements P return $credentials; } - - public function getPassword($credentials): ?string - { - return $credentials['password']; - } - - public function getUser($credentials): ?UserInterface - { - return $this->userProvider->loadUserByUsername($credentials['username']); - } - - public function getCsrfTokenId(): string - { - return $this->options['csrf_token_id']; - } - - public function getCsrfToken($credentials): ?string - { - return $credentials['csrf_token']; - } - - public function createAuthenticatedToken(UserInterface $user, $providerKey): TokenInterface - { - return new UsernamePasswordToken($user, null, $providerKey, $user->getRoles()); - } - - public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response - { - return $this->successHandler->onAuthenticationSuccess($request, $token); - } - - public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response - { - return $this->failureHandler->onAuthenticationFailure($request, $exception); - } } diff --git a/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php index 77480eea45..46eb6aa7bc 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php @@ -17,8 +17,14 @@ use Symfony\Component\HttpFoundation\Response; 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\AuthenticationServiceException; +use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; +use Symfony\Component\Security\Http\Authenticator\Passport\Passport; +use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; /** @@ -28,7 +34,7 @@ use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface * @final * @experimental in 5.1 */ -class HttpBasicAuthenticator implements AuthenticatorInterface, AuthenticationEntryPointInterface, PasswordAuthenticatedInterface +class HttpBasicAuthenticator implements AuthenticatorInterface, AuthenticationEntryPointInterface { private $realmName; private $userProvider; @@ -55,27 +61,30 @@ class HttpBasicAuthenticator implements AuthenticatorInterface, AuthenticationEn return $request->headers->has('PHP_AUTH_USER'); } - public function getCredentials(Request $request) + public function authenticate(Request $request): PassportInterface { - return [ - 'username' => $request->headers->get('PHP_AUTH_USER'), - 'password' => $request->headers->get('PHP_AUTH_PW', ''), - ]; + $username = $request->headers->get('PHP_AUTH_USER'); + $password = $request->headers->get('PHP_AUTH_PW', ''); + + $user = $this->userProvider->loadUserByUsername($username); + if (!$user instanceof UserInterface) { + throw new AuthenticationServiceException('The user provider must return a UserInterface object.'); + } + + $passport = new Passport($user, new PasswordCredentials($password)); + if ($this->userProvider instanceof PasswordUpgraderInterface) { + $passport->addBadge(new PasswordUpgradeBadge($password, $this->userProvider)); + } + + return $passport; } - public function getPassword($credentials): ?string + /** + * @param Passport $passport + */ + public function createAuthenticatedToken(PassportInterface $passport, $providerKey): TokenInterface { - return $credentials['password']; - } - - public function getUser($credentials): ?UserInterface - { - return $this->userProvider->loadUserByUsername($credentials['username']); - } - - public function createAuthenticatedToken(UserInterface $user, $providerKey): TokenInterface - { - return new UsernamePasswordToken($user, null, $providerKey, $user->getRoles()); + return new UsernamePasswordToken($passport->getUser(), null, $providerKey, $passport->getUser()->getRoles()); } public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey): ?Response diff --git a/src/Symfony/Component/Security/Http/Authenticator/InteractiveAuthenticatorInterface.php b/src/Symfony/Component/Security/Http/Authenticator/InteractiveAuthenticatorInterface.php index a2abf96e4a..7f26d82606 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/InteractiveAuthenticatorInterface.php +++ b/src/Symfony/Component/Security/Http/Authenticator/InteractiveAuthenticatorInterface.php @@ -11,10 +11,6 @@ namespace Symfony\Component\Security\Http\Authenticator; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; - /** * This is an extension of the authenticator interface that must * be used by interactive authenticators. diff --git a/src/Symfony/Component/Security/Http/Authenticator/JsonLoginAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/JsonLoginAuthenticator.php index f10e330923..924ed7fcca 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/JsonLoginAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/JsonLoginAuthenticator.php @@ -21,12 +21,18 @@ use Symfony\Component\PropertyAccess\PropertyAccessorInterface; 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\AuthenticationServiceException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Security; +use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; +use Symfony\Component\Security\Http\Authenticator\Passport\Passport; +use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; use Symfony\Component\Security\Http\HttpUtils; /** @@ -39,7 +45,7 @@ use Symfony\Component\Security\Http\HttpUtils; * @final * @experimental in 5.1 */ -class JsonLoginAuthenticator implements InteractiveAuthenticatorInterface, PasswordAuthenticatedInterface +class JsonLoginAuthenticator implements InteractiveAuthenticatorInterface { private $options; private $httpUtils; @@ -71,7 +77,51 @@ class JsonLoginAuthenticator implements InteractiveAuthenticatorInterface, Passw return true; } - public function getCredentials(Request $request) + public function authenticate(Request $request): PassportInterface + { + $credentials = $this->getCredentials($request); + $user = $this->userProvider->loadUserByUsername($credentials['username']); + if (!$user instanceof UserInterface) { + throw new AuthenticationServiceException('The user provider must return a UserInterface object.'); + } + + $passport = new Passport($user, new PasswordCredentials($credentials['password'])); + if ($this->userProvider instanceof PasswordUpgraderInterface) { + $passport->addBadge(new PasswordUpgradeBadge($credentials['password'], $this->userProvider)); + } + + return $passport; + } + + public function createAuthenticatedToken(PassportInterface $passport, string $providerKey): TokenInterface + { + return new UsernamePasswordToken($passport->getUser(), null, $providerKey, $passport->getUser()->getRoles()); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response + { + if (null === $this->successHandler) { + return null; // let the original request continue + } + + return $this->successHandler->onAuthenticationSuccess($request, $token); + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response + { + if (null === $this->failureHandler) { + return new JsonResponse(['error' => $exception->getMessageKey()], JsonResponse::HTTP_UNAUTHORIZED); + } + + return $this->failureHandler->onAuthenticationFailure($request, $exception); + } + + public function isInteractive(): bool + { + return true; + } + + private function getCredentials(Request $request) { $data = json_decode($request->getContent()); if (!$data instanceof \stdClass) { @@ -105,42 +155,4 @@ class JsonLoginAuthenticator implements InteractiveAuthenticatorInterface, Passw return $credentials; } - - public function getUser($credentials): ?UserInterface - { - return $this->userProvider->loadUserByUsername($credentials['username']); - } - - public function getPassword($credentials): ?string - { - return $credentials['password']; - } - - public function createAuthenticatedToken(UserInterface $user, string $providerKey): TokenInterface - { - return new UsernamePasswordToken($user, null, $providerKey, $user->getRoles()); - } - - public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response - { - if (null === $this->successHandler) { - return null; // let the original request continue - } - - return $this->successHandler->onAuthenticationSuccess($request, $token); - } - - public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response - { - if (null === $this->failureHandler) { - return new JsonResponse(['error' => $exception->getMessageKey()], JsonResponse::HTTP_UNAUTHORIZED); - } - - return $this->failureHandler->onAuthenticationFailure($request, $exception); - } - - public function isInteractive(): bool - { - return true; - } } diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/AnonymousPassport.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/AnonymousPassport.php new file mode 100644 index 0000000000..7cbc93e658 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/AnonymousPassport.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Authenticator\Passport; + +/** + * A passport used during anonymous authentication. + * + * @author Wouter de Jong + * + * @internal + * @experimental in 5.1 + */ +class AnonymousPassport implements PassportInterface +{ + use PassportTrait; +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/BadgeInterface.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/BadgeInterface.php new file mode 100644 index 0000000000..bc9ba7cbb5 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/BadgeInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Authenticator\Passport\Badge; + +/** + * Passport badges allow to add more information to a passport (e.g. a CSRF token). + * + * @author Wouter de Jong + * + * @experimental in 5.1 + */ +interface BadgeInterface +{ + /** + * Checks if this badge is resolved by the security system. + * + * After authentication, all badges must return `true` in this method in order + * for the authentication to succeed. + */ + public function isResolved(): bool; +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/CsrfTokenBadge.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/CsrfTokenBadge.php new file mode 100644 index 0000000000..9f0b4e5d89 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/CsrfTokenBadge.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Authenticator\Passport\Badge; + +use Symfony\Component\Security\Http\EventListener\CsrfProtectionListener; + +/** + * Adds automatic CSRF tokens checking capabilities to this authenticator. + * + * @see CsrfProtectionListener + * + * @author Wouter de Jong + * + * @final + * @experimental in5.1 + */ +class CsrfTokenBadge implements BadgeInterface +{ + private $resolved = false; + private $csrfTokenId; + private $csrfToken; + + /** + * @param string $csrfTokenId An arbitrary string used to generate the value of the CSRF token. + * Using a different string for each authenticator improves its security. + * @param string|null $csrfToken The CSRF token presented in the request, if any + */ + public function __construct(string $csrfTokenId, ?string $csrfToken) + { + $this->csrfTokenId = $csrfTokenId; + $this->csrfToken = $csrfToken; + } + + public function getCsrfTokenId(): string + { + return $this->csrfTokenId; + } + + public function getCsrfToken(): string + { + return $this->csrfToken; + } + + /** + * @internal + */ + public function markResolved(): void + { + $this->resolved = true; + } + + public function isResolved(): bool + { + return $this->resolved; + } +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/PasswordUpgradeBadge.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/PasswordUpgradeBadge.php new file mode 100644 index 0000000000..3812871da0 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/PasswordUpgradeBadge.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Authenticator\Passport\Badge; + +use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; + +/** + * Adds automatic password migration, if enabled and required in the password encoder. + * + * @see PasswordUpgraderInterface + * + * @author Wouter de Jong + * + * @final + * @experimental in 5.1 + */ +class PasswordUpgradeBadge implements BadgeInterface +{ + private $plaintextPassword; + private $passwordUpgrader; + + /** + * @param string $plaintextPassword The presented password, used in the rehash + * @param PasswordUpgraderInterface $passwordUpgrader The password upgrader, usually the UserProvider + */ + public function __construct(string $plaintextPassword, PasswordUpgraderInterface $passwordUpgrader) + { + $this->plaintextPassword = $plaintextPassword; + $this->passwordUpgrader = $passwordUpgrader; + } + + public function getPlaintextPassword(): string + { + return $this->plaintextPassword; + } + + public function getPasswordUpgrader(): PasswordUpgraderInterface + { + return $this->passwordUpgrader; + } + + /** + * @internal + */ + public function eraseCredentials() + { + $this->plaintextPassword = null; + } + + public function isResolved(): bool + { + return true; + } +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/PreAuthenticatedUserBadge.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/PreAuthenticatedUserBadge.php new file mode 100644 index 0000000000..7e0f330091 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/PreAuthenticatedUserBadge.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Authenticator\Passport\Badge; + +use Symfony\Component\Security\Http\Authenticator\AbstractPreAuthenticatedAuthenticator; + +/** + * Marks the authentication as being pre-authenticated. + * + * This disables pre-authentication user checkers. + * + * @see AbstractPreAuthenticatedAuthenticator + * + * @author Wouter de Jong + * + * @final + * @experimental in 5.1 + */ +class PreAuthenticatedUserBadge implements BadgeInterface +{ + public function isResolved(): bool + { + return true; + } +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticatorInterface.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/RememberMeBadge.php similarity index 61% rename from src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticatorInterface.php rename to src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/RememberMeBadge.php index d9eb6fa70b..dcee820442 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticatorInterface.php +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/RememberMeBadge.php @@ -9,23 +9,29 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Http\Authenticator; +namespace Symfony\Component\Security\Http\Authenticator\Passport\Badge; /** - * This interface must be extended if the authenticator supports remember me functionality. + * Adds support for remember me to this authenticator. * * Remember me cookie will be set if *all* of the following are met: - * A) SupportsRememberMe() returns true in the successful authenticator + * A) This badge is present in the Passport * 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 - * D) The onAuthenticationSuccess method returns a Response object + * D) The authentication process returns a success Response object * * @author Wouter de Jong + * + * @final + * @experimental in 5.1 */ -interface RememberMeAuthenticatorInterface +class RememberMeBadge implements BadgeInterface { - public function supportsRememberMe(): bool; + public function isResolved(): bool + { + return true; // remember me does not need to be explicitly resolved + } } diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/Credentials/CredentialsInterface.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/Credentials/CredentialsInterface.php new file mode 100644 index 0000000000..554fe7aff4 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/Credentials/CredentialsInterface.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Authenticator\Passport\Credentials; + +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface; + +/** + * Credentials are a special badge used to explicitly mark the + * credential check of an authenticator. + * + * @author Wouter de Jong + * + * @experimental in 5.1 + */ +interface CredentialsInterface extends BadgeInterface +{ +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/Credentials/CustomCredentials.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/Credentials/CustomCredentials.php new file mode 100644 index 0000000000..1a773f8afb --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/Credentials/CustomCredentials.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Authenticator\Passport\Credentials; + +use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\Security\Core\User\UserInterface; + +/** + * Implements credentials checking using a custom checker function. + * + * @author Wouter de Jong + * + * @final + * @experimental in 5.1 + */ +class CustomCredentials implements CredentialsInterface +{ + private $customCredentialsChecker; + private $credentials; + private $resolved = false; + + /** + * @param callable $customCredentialsChecker the check function. If this function does not return `true`, a + * BadCredentialsException is thrown. You may also throw a more + * specific exception in the function. + * @param $credentials + */ + public function __construct(callable $customCredentialsChecker, $credentials) + { + $this->customCredentialsChecker = $customCredentialsChecker; + $this->credentials = $credentials; + } + + public function executeCustomChecker(UserInterface $user): void + { + $checker = $this->customCredentialsChecker; + + if (true !== $checker($this->credentials, $user)) { + throw new BadCredentialsException('Credentials check failed as the callable passed to CustomCredentials did not return "true".'); + } + + $this->resolved = true; + } + + public function isResolved(): bool + { + return $this->resolved; + } +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/Credentials/PasswordCredentials.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/Credentials/PasswordCredentials.php new file mode 100644 index 0000000000..7630a67bd7 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/Credentials/PasswordCredentials.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Authenticator\Passport\Credentials; + +use Symfony\Component\Security\Core\Exception\LogicException; + +/** + * Implements password credentials. + * + * These plaintext passwords are checked by the UserPasswordEncoder during + * authentication. + * + * @author Wouter de Jong + * + * @final + * @experimental in 5.1 + */ +class PasswordCredentials implements CredentialsInterface +{ + private $password; + private $resolved = false; + + public function __construct(string $password) + { + $this->password = $password; + } + + public function getPassword(): string + { + if (null === $this->password) { + throw new LogicException('The credentials are erased as another listener already verified these credentials.'); + } + + return $this->password; + } + + /** + * @internal + */ + public function markResolved(): void + { + $this->resolved = true; + $this->password = null; + } + + public function isResolved(): bool + { + return $this->resolved; + } +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/Passport.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/Passport.php new file mode 100644 index 0000000000..a4ead01d14 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/Passport.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Authenticator\Passport; + +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CredentialsInterface; + +/** + * The default implementation for passports. + * + * @author Wouter de Jong + * + * @experimental in 5.1 + */ +class Passport implements UserPassportInterface +{ + use PassportTrait; + + protected $user; + + /** + * @param CredentialsInterface $credentials the credentials to check for this authentication, use + * SelfValidatingPassport if no credentials should be checked. + * @param BadgeInterface[] $badges + */ + public function __construct(UserInterface $user, CredentialsInterface $credentials, array $badges = []) + { + $this->user = $user; + + $this->addBadge($credentials); + foreach ($badges as $badge) { + $this->addBadge($badge); + } + } + + public function getUser(): UserInterface + { + return $this->user; + } +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/PassportInterface.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/PassportInterface.php new file mode 100644 index 0000000000..ac77969127 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/PassportInterface.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Authenticator\Passport; + +use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface; + +/** + * A Passport contains all security-related information that needs to be + * validated during authentication. + * + * A passport badge can be used to add any additional information to the + * passport. + * + * @author Wouter de Jong + * + * @experimental in 5.1 + */ +interface PassportInterface +{ + /** + * Adds a new security badge. + * + * A passport can hold only one instance of the same security badge. + * This method replaces the current badge if it is already set on this + * passport. + * + * @return $this + */ + public function addBadge(BadgeInterface $badge): self; + + public function hasBadge(string $badgeFqcn): bool; + + public function getBadge(string $badgeFqcn): ?BadgeInterface; + + /** + * Checks if all badges are marked as resolved. + * + * @throws BadCredentialsException when a badge is not marked as resolved + */ + public function checkIfCompletelyResolved(): void; +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/PassportTrait.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/PassportTrait.php new file mode 100644 index 0000000000..1cdd75546b --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/PassportTrait.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Authenticator\Passport; + +use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\Security\Core\Exception\InvalidArgumentException; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface; + +/** + * @author Wouter de Jong + * + * @experimental in 5.1 + */ +trait PassportTrait +{ + /** + * @var BadgeInterface[] + */ + private $badges = []; + + public function addBadge(BadgeInterface $badge): PassportInterface + { + $this->badges[\get_class($badge)] = $badge; + + return $this; + } + + public function hasBadge(string $badgeFqcn): bool + { + return isset($this->badges[$badgeFqcn]); + } + + public function getBadge(string $badgeFqcn): ?BadgeInterface + { + return $this->badges[$badgeFqcn] ?? null; + } + + public function checkIfCompletelyResolved(): void + { + foreach ($this->badges as $badge) { + if (!$badge->isResolved()) { + throw new BadCredentialsException(sprintf('Authentication failed security badge "%s" is not resolved, did you forget to register the correct listeners?', \get_class($badge))); + } + } + } +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/SelfValidatingPassport.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/SelfValidatingPassport.php new file mode 100644 index 0000000000..dd3ef6f962 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/SelfValidatingPassport.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Authenticator\Passport; + +use Symfony\Component\Security\Core\User\UserInterface; + +/** + * An implementation used when there are no credentials to be checked (e.g. + * API token authentication). + * + * @author Wouter de Jong + * + * @experimental in 5.1 + */ +class SelfValidatingPassport extends Passport +{ + public function __construct(UserInterface $user, array $badges = []) + { + $this->user = $user; + + foreach ($badges as $badge) { + $this->addBadge($badge); + } + } +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/UserPassportInterface.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/UserPassportInterface.php new file mode 100644 index 0000000000..f308c13252 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/UserPassportInterface.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Authenticator\Passport; + +use Symfony\Component\Security\Core\User\UserInterface; + +/** + * Represents a passport for a Security User. + * + * @author Wouter de Jong + * + * @experimental in 5.1 + */ +interface UserPassportInterface extends PassportInterface +{ + public function getUser(): UserInterface; +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/PasswordAuthenticatedInterface.php b/src/Symfony/Component/Security/Http/Authenticator/PasswordAuthenticatedInterface.php deleted file mode 100644 index 7386fc3373..0000000000 --- a/src/Symfony/Component/Security/Http/Authenticator/PasswordAuthenticatedInterface.php +++ /dev/null @@ -1,31 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Security\Http\Authenticator; - -/** - * This interface should be implemented when the authenticator - * uses a password to authenticate. - * - * The EncoderFactory will be used to automatically validate - * the password. - * - * @author Wouter de Jong - */ -interface PasswordAuthenticatedInterface -{ - /** - * Returns the clear-text password contained in credentials if any. - * - * @param mixed $credentials The user credentials - */ - public function getPassword($credentials): ?string; -} diff --git a/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php index 72c6ea5288..12a70d42b4 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php @@ -17,8 +17,10 @@ use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken; 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\Authenticator\Passport\PassportInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; use Symfony\Component\Security\Http\RememberMe\AbstractRememberMeServices; +use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; /** * The RememberMe *Authenticator* performs remember me authentication. @@ -33,22 +35,19 @@ use Symfony\Component\Security\Http\RememberMe\AbstractRememberMeServices; * * @final */ -class RememberMeAuthenticator implements InteractiveAuthenticatorInterface, CustomAuthenticatedInterface +class RememberMeAuthenticator implements InteractiveAuthenticatorInterface { private $rememberMeServices; private $secret; private $tokenStorage; - private $options = [ - 'secure' => false, - 'httponly' => true, - ]; + private $options = []; - public function __construct(AbstractRememberMeServices $rememberMeServices, string $secret, TokenStorageInterface $tokenStorage, array $options) + public function __construct(RememberMeServicesInterface $rememberMeServices, string $secret, TokenStorageInterface $tokenStorage, array $options) { $this->rememberMeServices = $rememberMeServices; $this->secret = $secret; $this->tokenStorage = $tokenStorage; - $this->options = array_merge($this->options, $options); + $this->options = $options; } public function supports(Request $request): ?bool @@ -62,7 +61,7 @@ class RememberMeAuthenticator implements InteractiveAuthenticatorInterface, Cust return false; } - if (!$request->cookies->has($this->options['name'])) { + if (isset($this->options['name']) && !$request->cookies->has($this->options['name'])) { return false; } @@ -70,31 +69,16 @@ class RememberMeAuthenticator implements InteractiveAuthenticatorInterface, Cust return null; } - public function getCredentials(Request $request) + public function authenticate(Request $request): PassportInterface { - return [ - 'cookie_parts' => explode(AbstractRememberMeServices::COOKIE_DELIMITER, base64_decode($request->cookies->get($this->options['name']))), - 'request' => $request, - ]; + $token = $this->rememberMeServices->autoLogin($request); + + return new SelfValidatingPassport($token->getUser()); } - /** - * @param array $credentials - */ - public function getUser($credentials): ?UserInterface + public function createAuthenticatedToken(PassportInterface $passport, string $providerKey): TokenInterface { - return $this->rememberMeServices->performLogin($credentials['cookie_parts'], $credentials['request']); - } - - public function checkCredentials($credentials, UserInterface $user): bool - { - // remember me always is valid (if a user could be found) - return true; - } - - public function createAuthenticatedToken(UserInterface $user, string $providerKey): TokenInterface - { - return new RememberMeToken($user, $providerKey, $this->secret); + return new RememberMeToken($passport->getUser(), $providerKey, $this->secret); } public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response diff --git a/src/Symfony/Component/Security/Http/Authenticator/RemoteUserAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/RemoteUserAuthenticator.php index 3a01087767..140b6c271e 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/RemoteUserAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/RemoteUserAuthenticator.php @@ -24,6 +24,8 @@ use Symfony\Component\Security\Core\User\UserProviderInterface; * @author Fabien Potencier * @author Maxime Douailin * + * @final + * * @internal in Symfony 5.1 */ class RemoteUserAuthenticator extends AbstractPreAuthenticatedAuthenticator diff --git a/src/Symfony/Component/Security/Http/Authenticator/TokenAuthenticatedInterface.php b/src/Symfony/Component/Security/Http/Authenticator/TokenAuthenticatedInterface.php deleted file mode 100644 index 88d0d7f965..0000000000 --- a/src/Symfony/Component/Security/Http/Authenticator/TokenAuthenticatedInterface.php +++ /dev/null @@ -1,33 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Security\Http\Authenticator; - -/** - * This interface should be implemented when the authenticator - * doesn't need to check credentials (e.g. when using API tokens) - * - * @author Wouter de Jong - */ -interface TokenAuthenticatedInterface -{ - /** - * Extracts the token from the credentials. - * - * If you return null, the credentials will not be marked as - * valid and a BadCredentialsException is thrown. - * - * @param mixed $credentials The user credentials - * - * @return mixed|null the token - if any - or null otherwise - */ - public function getToken($credentials); -} diff --git a/src/Symfony/Component/Security/Http/Authenticator/X509Authenticator.php b/src/Symfony/Component/Security/Http/Authenticator/X509Authenticator.php index d482579d05..c76f3f94e5 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/X509Authenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/X509Authenticator.php @@ -24,7 +24,7 @@ use Symfony\Component\Security\Core\User\UserProviderInterface; * @author Wouter de Jong * @author Fabien Potencier * - * @internal + * @final * @experimental in Symfony 5.1 */ class X509Authenticator extends AbstractPreAuthenticatedAuthenticator diff --git a/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php b/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php index 6e48e171b6..80f740480b 100644 --- a/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php +++ b/src/Symfony/Component/Security/Http/Event/LoginSuccessEvent.php @@ -5,7 +5,11 @@ namespace Symfony\Component\Security\Http\Event; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Exception\LogicException; +use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\UserPassportInterface; use Symfony\Contracts\EventDispatcher\Event; /** @@ -21,14 +25,16 @@ use Symfony\Contracts\EventDispatcher\Event; class LoginSuccessEvent extends Event { private $authenticator; + private $passport; private $authenticatedToken; private $request; private $response; private $providerKey; - public function __construct(AuthenticatorInterface $authenticator, TokenInterface $authenticatedToken, Request $request, ?Response $response, string $providerKey) + public function __construct(AuthenticatorInterface $authenticator, PassportInterface $passport, TokenInterface $authenticatedToken, Request $request, ?Response $response, string $providerKey) { $this->authenticator = $authenticator; + $this->passport = $passport; $this->authenticatedToken = $authenticatedToken; $this->request = $request; $this->response = $response; @@ -40,6 +46,20 @@ class LoginSuccessEvent extends Event return $this->authenticator; } + public function getPassport(): PassportInterface + { + return $this->passport; + } + + public function getUser(): UserInterface + { + if (!$this->passport instanceof UserPassportInterface) { + throw new LogicException(sprintf('Cannot call "%s" as the authenticator ("%s") did not set a user.', __METHOD__, \get_class($this->authenticator))); + } + + return $this->passport->getUser(); + } + public function getAuthenticatedToken(): TokenInterface { return $this->authenticatedToken; diff --git a/src/Symfony/Component/Security/Http/Event/VerifyAuthenticatorCredentialsEvent.php b/src/Symfony/Component/Security/Http/Event/VerifyAuthenticatorCredentialsEvent.php index cc37bf33f2..eac7f03741 100644 --- a/src/Symfony/Component/Security/Http/Event/VerifyAuthenticatorCredentialsEvent.php +++ b/src/Symfony/Component/Security/Http/Event/VerifyAuthenticatorCredentialsEvent.php @@ -5,6 +5,7 @@ namespace Symfony\Component\Security\Http\Event; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; use Symfony\Contracts\EventDispatcher\Event; /** @@ -19,15 +20,12 @@ use Symfony\Contracts\EventDispatcher\Event; class VerifyAuthenticatorCredentialsEvent extends Event { private $authenticator; - private $user; - private $credentials; - private $credentialsValid = false; + private $passport; - public function __construct(AuthenticatorInterface $authenticator, $credentials, ?UserInterface $user) + public function __construct(AuthenticatorInterface $authenticator, PassportInterface $passport) { $this->authenticator = $authenticator; - $this->credentials = $credentials; - $this->user = $user; + $this->passport = $passport; } public function getAuthenticator(): AuthenticatorInterface @@ -35,23 +33,8 @@ class VerifyAuthenticatorCredentialsEvent extends Event return $this->authenticator; } - public function getCredentials() + public function getPassport(): PassportInterface { - return $this->credentials; - } - - public function getUser(): ?UserInterface - { - return $this->user; - } - - public function setCredentialsValid(bool $validated = true): void - { - $this->credentialsValid = $validated; - } - - public function areCredentialsValid(): bool - { - return $this->credentialsValid; + return $this->passport; } } diff --git a/src/Symfony/Component/Security/Http/EventListener/CsrfProtectionListener.php b/src/Symfony/Component/Security/Http/EventListener/CsrfProtectionListener.php index fcde792452..65c8ffa3e3 100644 --- a/src/Symfony/Component/Security/Http/EventListener/CsrfProtectionListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/CsrfProtectionListener.php @@ -15,9 +15,15 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; use Symfony\Component\Security\Csrf\CsrfToken; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; -use Symfony\Component\Security\Http\Authenticator\CsrfProtectedAuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge; use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; +/** + * @author Wouter de Jong + * + * @final + * @experimental in 5.1 + */ class CsrfProtectionListener implements EventSubscriberInterface { private $csrfTokenManager; @@ -29,20 +35,24 @@ class CsrfProtectionListener implements EventSubscriberInterface public function verifyCredentials(VerifyAuthenticatorCredentialsEvent $event): void { - $authenticator = $event->getAuthenticator(); - if (!$authenticator instanceof CsrfProtectedAuthenticatorInterface) { + $passport = $event->getPassport(); + if (!$passport->hasBadge(CsrfTokenBadge::class)) { return; } - $csrfTokenValue = $authenticator->getCsrfToken($event->getCredentials()); - if (null === $csrfTokenValue) { + /** @var CsrfTokenBadge $badge */ + $badge = $passport->getBadge(CsrfTokenBadge::class); + if ($badge->isResolved()) { return; } - $csrfToken = new CsrfToken($authenticator->getCsrfTokenId(), $csrfTokenValue); + $csrfToken = new CsrfToken($badge->getCsrfTokenId(), $badge->getCsrfToken()); + if (false === $this->csrfTokenManager->isTokenValid($csrfToken)) { throw new InvalidCsrfTokenException('Invalid CSRF token.'); } + + $badge->markResolved(); } public static function getSubscribedEvents(): array diff --git a/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php b/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php index 28800e6260..0d22bf22ca 100644 --- a/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php @@ -4,10 +4,9 @@ namespace Symfony\Component\Security\Http\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; -use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; -use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Component\Security\Http\Authenticator\PasswordAuthenticatedInterface; -use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\UserPassportInterface; +use Symfony\Component\Security\Http\Event\LoginSuccessEvent; /** * @author Wouter de Jong @@ -24,37 +23,33 @@ class PasswordMigratingListener implements EventSubscriberInterface $this->encoderFactory = $encoderFactory; } - public function onCredentialsVerification(VerifyAuthenticatorCredentialsEvent $event): void + public function onLoginSuccess(LoginSuccessEvent $event): void { - if (!$event->areCredentialsValid()) { - // Do not migrate password that are not validated + $passport = $event->getPassport(); + if (!$passport instanceof UserPassportInterface || !$passport->hasBadge(PasswordUpgradeBadge::class)) { return; } - $authenticator = $event->getAuthenticator(); - if (!$authenticator instanceof PasswordAuthenticatedInterface || !$authenticator instanceof PasswordUpgraderInterface) { - return; - } - - if (null === $password = $authenticator->getPassword($event->getCredentials())) { - return; - } - - $user = $event->getUser(); - if (!$user instanceof UserInterface) { + /** @var PasswordUpgradeBadge $badge */ + $badge = $passport->getBadge(PasswordUpgradeBadge::class); + $plaintextPassword = $badge->getPlaintextPassword(); + $badge->eraseCredentials(); + + if ('' === $plaintextPassword) { return; } + $user = $passport->getUser(); $passwordEncoder = $this->encoderFactory->getEncoder($user); if (!$passwordEncoder->needsRehash($user->getPassword())) { return; } - $authenticator->upgradePassword($user, $passwordEncoder->encodePassword($password, $user->getSalt())); + $badge->getPasswordUpgrader()->upgradePassword($user, $passwordEncoder->encodePassword($plaintextPassword, $user->getSalt())); } public static function getSubscribedEvents(): array { - return [VerifyAuthenticatorCredentialsEvent::class => ['onCredentialsVerification', -128]]; + return [LoginSuccessEvent::class => 'onLoginSuccess']; } } diff --git a/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php b/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php index 269d232786..da582a7cc6 100644 --- a/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/RememberMeListener.php @@ -4,8 +4,7 @@ namespace Symfony\Component\Security\Http\EventListener; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; -use Symfony\Component\Security\Http\Authenticator\RememberMeAuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge; use Symfony\Component\Security\Http\Event\LoginFailureEvent; use Symfony\Component\Security\Http\Event\LoginSuccessEvent; use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; @@ -34,13 +33,12 @@ class RememberMeListener implements EventSubscriberInterface $this->logger = $logger; } - public function onSuccessfulLogin(LoginSuccessEvent $event): void { - $authenticator = $event->getAuthenticator(); - if (!$authenticator instanceof RememberMeAuthenticatorInterface || !$authenticator->supportsRememberMe()) { + $passport = $event->getPassport(); + if (!$passport->hasBadge(RememberMeBadge::class)) { if (null !== $this->logger) { - $this->logger->debug('Remember me skipped: your authenticator does not support it.', ['authenticator' => \get_class($authenticator)]); + $this->logger->debug('Remember me skipped: your authenticator does not support it.', ['authenticator' => \get_class($event->getAuthenticator())]); } return; @@ -48,7 +46,7 @@ class RememberMeListener implements EventSubscriberInterface if (null === $event->getResponse()) { if (null !== $this->logger) { - $this->logger->debug('Remember me skipped: the authenticator did not set a success response.', ['authenticator' => \get_class($authenticator)]); + $this->logger->debug('Remember me skipped: the authenticator did not set a success response.', ['authenticator' => \get_class($event->getAuthenticator())]); } return; diff --git a/src/Symfony/Component/Security/Http/EventListener/UserCheckerListener.php b/src/Symfony/Component/Security/Http/EventListener/UserCheckerListener.php index 34fdfdf84d..fbcc0bd549 100644 --- a/src/Symfony/Component/Security/Http/EventListener/UserCheckerListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/UserCheckerListener.php @@ -4,7 +4,9 @@ namespace Symfony\Component\Security\Http\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Security\Core\User\UserCheckerInterface; -use Symfony\Component\Security\Http\Authenticator\AbstractPreAuthenticatedAuthenticator; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PreAuthenticatedUserBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\UserPassportInterface; +use Symfony\Component\Security\Http\Event\LoginSuccessEvent; use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; /** @@ -24,29 +26,29 @@ class UserCheckerListener implements EventSubscriberInterface public function preCredentialsVerification(VerifyAuthenticatorCredentialsEvent $event): void { - if (null === $event->getUser() || $event->getAuthenticator() instanceof AbstractPreAuthenticatedAuthenticator) { + $passport = $event->getPassport(); + if (!$passport instanceof UserPassportInterface || $passport->hasBadge(PreAuthenticatedUserBadge::class)) { return; } - $this->userChecker->checkPreAuth($event->getUser()); + $this->userChecker->checkPreAuth($passport->getUser()); } - public function postCredentialsVerification(VerifyAuthenticatorCredentialsEvent $event): void + public function postCredentialsVerification(LoginSuccessEvent $event): void { - if (null === $event->getUser() || !$event->areCredentialsValid()) { + $passport = $event->getPassport(); + if (!$passport instanceof UserPassportInterface || null === $passport->getUser()) { return; } - $this->userChecker->checkPostAuth($event->getUser()); + $this->userChecker->checkPostAuth($passport->getUser()); } public static function getSubscribedEvents(): array { return [ - VerifyAuthenticatorCredentialsEvent::class => [ - ['preCredentialsVerification', 256], - ['preCredentialsVerification', 32] - ], + VerifyAuthenticatorCredentialsEvent::class => [['preCredentialsVerification', 256]], + LoginSuccessEvent::class => ['postCredentialsVerification', 256], ]; } } diff --git a/src/Symfony/Component/Security/Http/EventListener/VerifyAuthenticatorCredentialsListener.php b/src/Symfony/Component/Security/Http/EventListener/VerifyAuthenticatorCredentialsListener.php index 77bbb39ec9..0287dc4f5d 100644 --- a/src/Symfony/Component/Security/Http/EventListener/VerifyAuthenticatorCredentialsListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/VerifyAuthenticatorCredentialsListener.php @@ -5,10 +5,9 @@ namespace Symfony\Component\Security\Http\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\Security\Core\Exception\BadCredentialsException; -use Symfony\Component\Security\Core\Exception\LogicException; -use Symfony\Component\Security\Http\Authenticator\CustomAuthenticatedInterface; -use Symfony\Component\Security\Http\Authenticator\PasswordAuthenticatedInterface; -use Symfony\Component\Security\Http\Authenticator\TokenAuthenticatedInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials; +use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; +use Symfony\Component\Security\Http\Authenticator\Passport\UserPassportInterface; use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; /** @@ -31,44 +30,46 @@ class VerifyAuthenticatorCredentialsListener implements EventSubscriberInterface public function onAuthenticating(VerifyAuthenticatorCredentialsEvent $event): void { - if ($event->areCredentialsValid()) { - return; - } - - $authenticator = $event->getAuthenticator(); - if ($authenticator instanceof PasswordAuthenticatedInterface) { + $passport = $event->getPassport(); + if ($passport instanceof UserPassportInterface && $passport->hasBadge(PasswordCredentials::class)) { // Use the password encoder to validate the credentials - $user = $event->getUser(); - $presentedPassword = $authenticator->getPassword($event->getCredentials()); + $user = $passport->getUser(); + /** @var PasswordCredentials $badge */ + $badge = $passport->getBadge(PasswordCredentials::class); + + if ($badge->isResolved()) { + return; + } + + $presentedPassword = $badge->getPassword(); if ('' === $presentedPassword) { throw new BadCredentialsException('The presented password cannot be empty.'); } if (null === $user->getPassword()) { + throw new BadCredentialsException('The presented password is invalid.'); + } + + if (!$this->encoderFactory->getEncoder($user)->isPasswordValid($user->getPassword(), $presentedPassword, $user->getSalt())) { + throw new BadCredentialsException('The presented password is invalid.'); + } + + $badge->markResolved(); + + return; + } + + if ($passport->hasBadge(CustomCredentials::class)) { + /** @var CustomCredentials $badge */ + $badge = $passport->getBadge(CustomCredentials::class); + if ($badge->isResolved()) { return; } - $event->setCredentialsValid($this->encoderFactory->getEncoder($user)->isPasswordValid($user->getPassword(), $presentedPassword, $user->getSalt())); + $badge->executeCustomChecker($passport->getUser()); return; } - - if ($authenticator instanceof TokenAuthenticatedInterface) { - if (null !== $authenticator->getToken($event->getCredentials())) { - // Token based authenticators do not have a credential validation step - $event->setCredentialsValid(); - } - - return; - } - - if ($authenticator instanceof CustomAuthenticatedInterface) { - $event->setCredentialsValid($authenticator->checkCredentials($event->getCredentials(), $event->getUser())); - - return; - } - - throw new LogicException(sprintf('Authenticator %s does not have valid credentials. Authenticators must implement one of the authenticated interfaces (%s, %s or %s).', \get_class($authenticator), PasswordAuthenticatedInterface::class, TokenAuthenticatedInterface::class, CustomAuthenticatedInterface::class)); } public static function getSubscribedEvents(): array diff --git a/src/Symfony/Component/Security/Http/RememberMe/AbstractRememberMeServices.php b/src/Symfony/Component/Security/Http/RememberMe/AbstractRememberMeServices.php index e9065d7f52..22f9dde14b 100644 --- a/src/Symfony/Component/Security/Http/RememberMe/AbstractRememberMeServices.php +++ b/src/Symfony/Component/Security/Http/RememberMe/AbstractRememberMeServices.php @@ -89,11 +89,6 @@ abstract class AbstractRememberMeServices implements RememberMeServicesInterface return $this->secret; } - public function performLogin(array $cookieParts, Request $request): UserInterface - { - return $this->processAutoLoginCookie($cookieParts, $request); - } - /** * Implementation of RememberMeServicesInterface. Detects whether a remember-me * cookie was set, decodes it, and hands it to subclasses for further processing. diff --git a/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php b/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php index 7343d79788..2cf7994db7 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php @@ -18,10 +18,12 @@ 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\BadCredentialsException; -use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; -use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Http\Authentication\AuthenticatorManager; use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; +use Symfony\Component\Security\Http\Authenticator\Passport\Passport; +use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; class AuthenticatorManagerTest extends TestCase @@ -38,7 +40,7 @@ class AuthenticatorManagerTest extends TestCase $this->tokenStorage = $this->createMock(TokenStorageInterface::class); $this->eventDispatcher = new EventDispatcher(); $this->request = new Request(); - $this->user = $this->createMock(UserInterface::class); + $this->user = new User('wouter', null); $this->token = $this->createMock(TokenInterface::class); $this->response = $this->createMock(Response::class); } @@ -73,7 +75,7 @@ class AuthenticatorManagerTest extends TestCase $authenticator = $this->createAuthenticator(false); $this->request->attributes->set('_guard_authenticators', [$authenticator]); - $authenticator->expects($this->never())->method('getCredentials'); + $authenticator->expects($this->never())->method('authenticate'); $manager = $this->createManager([$authenticator]); $manager->authenticateRequest($this->request); @@ -88,17 +90,14 @@ class AuthenticatorManagerTest extends TestCase $this->request->attributes->set('_guard_authenticators', $authenticators); $matchingAuthenticator = $authenticators[$matchingAuthenticatorIndex]; - $authenticators[($matchingAuthenticatorIndex + 1) % 2]->expects($this->never())->method('getCredentials'); + $authenticators[($matchingAuthenticatorIndex + 1) % 2]->expects($this->never())->method('authenticate'); - $matchingAuthenticator->expects($this->any())->method('getCredentials')->willReturn(['password' => 'pa$$']); - $matchingAuthenticator->expects($this->any())->method('getUser')->willReturn($this->user); + $matchingAuthenticator->expects($this->any())->method('authenticate')->willReturn(new SelfValidatingPassport($this->user)); $listenerCalled = false; $this->eventDispatcher->addListener(VerifyAuthenticatorCredentialsEvent::class, function (VerifyAuthenticatorCredentialsEvent $event) use (&$listenerCalled, $matchingAuthenticator) { - if ($event->getAuthenticator() === $matchingAuthenticator && $event->getCredentials() === ['password' => 'pa$$'] && $event->getUser() === $this->user) { + if ($event->getAuthenticator() === $matchingAuthenticator && $event->getPassport()->getUser() === $this->user) { $listenerCalled = true; - - $event->setCredentialsValid(true); } }); $matchingAuthenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token); @@ -116,29 +115,12 @@ class AuthenticatorManagerTest extends TestCase yield [1]; } - public function testUserNotFound() - { - $authenticator = $this->createAuthenticator(); - $this->request->attributes->set('_guard_authenticators', [$authenticator]); - - $authenticator->expects($this->any())->method('getCredentials')->willReturn(['username' => 'john']); - $authenticator->expects($this->any())->method('getUser')->with(['username' => 'john'])->willReturn(null); - - $authenticator->expects($this->once()) - ->method('onAuthenticationFailure') - ->with($this->request, $this->isInstanceOf(UsernameNotFoundException::class)); - - $manager = $this->createManager([$authenticator]); - $manager->authenticateRequest($this->request); - } - public function testNoCredentialsValidated() { $authenticator = $this->createAuthenticator(); $this->request->attributes->set('_guard_authenticators', [$authenticator]); - $authenticator->expects($this->any())->method('getCredentials')->willReturn(['username' => 'john']); - $authenticator->expects($this->any())->method('getUser')->willReturn($this->user); + $authenticator->expects($this->any())->method('authenticate')->willReturn(new Passport($this->user, new PasswordCredentials('pass'))); $authenticator->expects($this->once()) ->method('onAuthenticationFailure') @@ -156,11 +138,7 @@ class AuthenticatorManagerTest extends TestCase $authenticator = $this->createAuthenticator(); $this->request->attributes->set('_guard_authenticators', [$authenticator]); - $authenticator->expects($this->any())->method('getCredentials')->willReturn(['username' => 'john']); - $authenticator->expects($this->any())->method('getUser')->willReturn($this->user); - $this->eventDispatcher->addListener(VerifyAuthenticatorCredentialsEvent::class, function (VerifyAuthenticatorCredentialsEvent $event) { - $event->setCredentialsValid(true); - }); + $authenticator->expects($this->any())->method('authenticate')->willReturn(new SelfValidatingPassport($this->user)); $authenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token); @@ -194,12 +172,7 @@ class AuthenticatorManagerTest extends TestCase $authenticator->expects($this->any())->method('isInteractive')->willReturn(true); $this->request->attributes->set('_guard_authenticators', [$authenticator]); - $authenticator->expects($this->any())->method('getCredentials')->willReturn(['password' => 'pa$$']); - $authenticator->expects($this->any())->method('getUser')->willReturn($this->user); - - $this->eventDispatcher->addListener(VerifyAuthenticatorCredentialsEvent::class, function (VerifyAuthenticatorCredentialsEvent $event) { - $event->setCredentialsValid(true); - }); + $authenticator->expects($this->any())->method('authenticate')->willReturn(new SelfValidatingPassport($this->user)); $authenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token); $this->tokenStorage->expects($this->once())->method('setToken')->with($this->token); diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/AnonymousAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/AnonymousAuthenticatorTest.php index f5d1cfdf98..d5593bb375 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/AnonymousAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/AnonymousAuthenticatorTest.php @@ -14,7 +14,6 @@ namespace Symfony\Component\Security\Http\Tests\Authenticator; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; -use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Http\Authenticator\AnonymousAuthenticator; class AnonymousAuthenticatorTest extends TestCase @@ -46,14 +45,9 @@ class AnonymousAuthenticatorTest extends TestCase yield [false, false]; } - public function testAlwaysValidCredentials() - { - $this->assertTrue($this->authenticator->checkCredentials([], $this->createMock(UserInterface::class))); - } - public function testAuthenticatedToken() { - $token = $this->authenticator->createAuthenticatedToken($this->authenticator->getUser([]), 'main'); + $token = $this->authenticator->createAuthenticatedToken($this->authenticator->authenticate($this->request), 'main'); $this->assertTrue($token->isAuthenticated()); $this->assertEquals('anon.', $token->getUser()); diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/FormLoginAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/FormLoginAuthenticatorTest.php index 3012da746d..9ab9055455 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/FormLoginAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/FormLoginAuthenticatorTest.php @@ -16,10 +16,14 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Security; +use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; +use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; use Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge; use Symfony\Component\Security\Http\HttpUtils; class FormLoginAuthenticatorTest extends TestCase @@ -27,11 +31,13 @@ class FormLoginAuthenticatorTest extends TestCase private $userProvider; private $successHandler; private $failureHandler; + /** @var FormLoginAuthenticator */ private $authenticator; protected function setUp(): void { $this->userProvider = $this->createMock(UserProviderInterface::class); + $this->userProvider->expects($this->any())->method('loadUserByUsername')->willReturn(new User('test', 's$cr$t')); $this->successHandler = $this->createMock(AuthenticationSuccessHandlerInterface::class); $this->failureHandler = $this->createMock(AuthenticationFailureHandlerInterface::class); } @@ -48,11 +54,11 @@ class FormLoginAuthenticatorTest extends TestCase $this->expectExceptionMessage('Invalid username.'); } - $request = Request::create('/login_check', 'POST', ['_username' => $username]); + $request = Request::create('/login_check', 'POST', ['_username' => $username, '_password' => 's$cr$t']); $request->setSession($this->createSession()); $this->setUpAuthenticator(); - $this->authenticator->getCredentials($request); + $this->authenticator->authenticate($request); } public function provideUsernamesForLength() @@ -73,7 +79,7 @@ class FormLoginAuthenticatorTest extends TestCase $request->setSession($this->createSession()); $this->setUpAuthenticator(['post_only' => $postOnly]); - $this->authenticator->getCredentials($request); + $this->authenticator->authenticate($request); } /** @@ -88,7 +94,7 @@ class FormLoginAuthenticatorTest extends TestCase $request->setSession($this->createSession()); $this->setUpAuthenticator(['post_only' => $postOnly]); - $this->authenticator->getCredentials($request); + $this->authenticator->authenticate($request); } /** @@ -103,22 +109,22 @@ class FormLoginAuthenticatorTest extends TestCase $request->setSession($this->createSession()); $this->setUpAuthenticator(['post_only' => $postOnly]); - $this->authenticator->getCredentials($request); + $this->authenticator->authenticate($request); } /** * @dataProvider postOnlyDataProvider */ - public function testHandleNonStringUsernameWith__toString($postOnly) + public function testHandleNonStringUsernameWithToString($postOnly) { $usernameObject = $this->getMockBuilder(DummyUserClass::class)->getMock(); $usernameObject->expects($this->once())->method('__toString')->willReturn('someUsername'); - $request = Request::create('/login_check', 'POST', ['_username' => $usernameObject]); + $request = Request::create('/login_check', 'POST', ['_username' => $usernameObject, '_password' => 's$cr$t']); $request->setSession($this->createSession()); $this->setUpAuthenticator(['post_only' => $postOnly]); - $this->authenticator->getCredentials($request); + $this->authenticator->authenticate($request); } public function postOnlyDataProvider() @@ -127,6 +133,31 @@ class FormLoginAuthenticatorTest extends TestCase yield [false]; } + public function testCsrfProtection() + { + $request = Request::create('/login_check', 'POST', ['_username' => 'wouter', '_password' => 's$cr$t']); + $request->setSession($this->createSession()); + + $this->setUpAuthenticator(['enable_csrf' => true]); + $passport = $this->authenticator->authenticate($request); + $this->assertTrue($passport->hasBadge(CsrfTokenBadge::class)); + } + + public function testUpgradePassword() + { + $request = Request::create('/login_check', 'POST', ['_username' => 'wouter', '_password' => 's$cr$t']); + $request->setSession($this->createSession()); + + $this->userProvider = $this->createMock([UserProviderInterface::class, PasswordUpgraderInterface::class]); + $this->userProvider->expects($this->any())->method('loadUserByUsername')->willReturn(new User('test', 's$cr$t')); + + $this->setUpAuthenticator(); + $passport = $this->authenticator->authenticate($request); + $this->assertTrue($passport->hasBadge(PasswordUpgradeBadge::class)); + $badge = $passport->getBadge(PasswordUpgradeBadge::class); + $this->assertEquals('s$cr$t', $badge->getPlaintextPassword()); + } + private function setUpAuthenticator(array $options = []) { $this->authenticator = new FormLoginAuthenticator(new HttpUtils(), $this->userProvider, $this->successHandler, $this->failureHandler, $options); diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/HttpBasicAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/HttpBasicAuthenticatorTest.php index e2ac0ac991..693eb320ab 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/HttpBasicAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/HttpBasicAuthenticatorTest.php @@ -2,15 +2,16 @@ namespace Symfony\Component\Security\Http\Tests\Authenticator; -use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface; -use Symfony\Component\Security\Core\Exception\BadCredentialsException; -use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; +use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Authenticator\HttpBasicAuthenticator; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; class HttpBasicAuthenticatorTest extends TestCase { @@ -39,25 +40,16 @@ class HttpBasicAuthenticatorTest extends TestCase 'PHP_AUTH_PW' => 'ThePassword', ]); - $credentials = $this->authenticator->getCredentials($request); - $this->assertEquals([ - 'username' => 'TheUsername', - 'password' => 'ThePassword', - ], $credentials); - - $mockedUser = $this->getMockBuilder(UserInterface::class)->getMock(); - $mockedUser->expects($this->any())->method('getPassword')->willReturn('ThePassword'); - $this->userProvider ->expects($this->any()) ->method('loadUserByUsername') ->with('TheUsername') - ->willReturn($mockedUser); + ->willReturn($user = new User('TheUsername', 'ThePassword')); - $user = $this->authenticator->getUser($credentials); - $this->assertSame($mockedUser, $user); + $passport = $this->authenticator->authenticate($request); + $this->assertEquals('ThePassword', $passport->getBadge(PasswordCredentials::class)->getPassword()); - $this->assertEquals('ThePassword', $this->authenticator->getPassword($credentials)); + $this->assertSame($user, $passport->getUser()); } /** @@ -77,4 +69,21 @@ class HttpBasicAuthenticatorTest extends TestCase [['PHP_AUTH_PW' => 'ThePassword']], ]; } + + public function testUpgradePassword() + { + $request = new Request([], [], [], [], [], [ + 'PHP_AUTH_USER' => 'TheUsername', + 'PHP_AUTH_PW' => 'ThePassword', + ]); + + $this->userProvider = $this->createMock([UserProviderInterface::class, PasswordUpgraderInterface::class]); + $this->userProvider->expects($this->any())->method('loadUserByUsername')->willReturn(new User('test', 's$cr$t')); + $authenticator = new HttpBasicAuthenticator('test', $this->userProvider); + + $passport = $authenticator->authenticate($request); + $this->assertTrue($passport->hasBadge(PasswordUpgradeBadge::class)); + $badge = $passport->getBadge(PasswordUpgradeBadge::class); + $this->assertEquals('ThePassword', $badge->getPlaintextPassword()); + } } diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/JsonLoginAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/JsonLoginAuthenticatorTest.php index 84ff61781f..0f1967600a 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/JsonLoginAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/JsonLoginAuthenticatorTest.php @@ -16,8 +16,10 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Security; +use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Authenticator\JsonLoginAuthenticator; +use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; use Symfony\Component\Security\Http\HttpUtils; class JsonLoginAuthenticatorTest extends TestCase @@ -66,39 +68,45 @@ class JsonLoginAuthenticatorTest extends TestCase yield [Request::create('/login', 'GET', [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json']), false]; } - public function testGetCredentials() + public function testAuthenticate() { $this->setUpAuthenticator(); + $this->userProvider->expects($this->once())->method('loadUserByUsername')->with('dunglas')->willReturn(new User('dunglas', 'pa$$')); + $request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"username": "dunglas", "password": "foo"}'); - $this->assertEquals(['username' => 'dunglas', 'password' => 'foo'], $this->authenticator->getCredentials($request)); + $passport = $this->authenticator->authenticate($request); + $this->assertEquals('foo', $passport->getBadge(PasswordCredentials::class)->getPassword()); } - public function testGetCredentialsCustomPath() + public function testAuthenticateWithCustomPath() { $this->setUpAuthenticator([ 'username_path' => 'authentication.username', 'password_path' => 'authentication.password', ]); + $this->userProvider->expects($this->once())->method('loadUserByUsername')->with('dunglas')->willReturn(new User('dunglas', 'pa$$')); + $request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"authentication": {"username": "dunglas", "password": "foo"}}'); - $this->assertEquals(['username' => 'dunglas', 'password' => 'foo'], $this->authenticator->getCredentials($request)); + $passport = $this->authenticator->authenticate($request); + $this->assertEquals('foo', $passport->getBadge(PasswordCredentials::class)->getPassword()); } /** - * @dataProvider provideInvalidGetCredentialsData + * @dataProvider provideInvalidAuthenticateData */ - public function testGetCredentialsInvalid($request, $errorMessage, $exceptionType = BadRequestHttpException::class) + public function testAuthenticateInvalid($request, $errorMessage, $exceptionType = BadRequestHttpException::class) { $this->expectException($exceptionType); $this->expectExceptionMessage($errorMessage); $this->setUpAuthenticator(); - $this->authenticator->getCredentials($request); + $this->authenticator->authenticate($request); } - public function provideInvalidGetCredentialsData() + public function provideInvalidAuthenticateData() { $request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json']); yield [$request, 'Invalid JSON.']; diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/RememberMeAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/RememberMeAuthenticatorTest.php index 9bd11ab62d..d95e681281 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/RememberMeAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/RememberMeAuthenticatorTest.php @@ -14,11 +14,13 @@ namespace Symfony\Component\Security\Http\Tests\Authenticator; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Http\Authenticator\RememberMeAuthenticator; use Symfony\Component\Security\Http\RememberMe\AbstractRememberMeServices; +use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; class RememberMeAuthenticatorTest extends TestCase { @@ -29,7 +31,7 @@ class RememberMeAuthenticatorTest extends TestCase protected function setUp(): void { - $this->rememberMeServices = $this->createMock(AbstractRememberMeServices::class); + $this->rememberMeServices = $this->createMock(RememberMeServicesInterface::class); $this->tokenStorage = $this->createMock(TokenStorage::class); $this->authenticator = new RememberMeAuthenticator($this->rememberMeServices, 's3cr3t', $this->tokenStorage, [ 'name' => '_remember_me_cookie', @@ -67,22 +69,14 @@ class RememberMeAuthenticatorTest extends TestCase public function testAuthenticate() { - $credentials = $this->authenticator->getCredentials($this->request); - $this->assertEquals(['part1', 'part2'], $credentials['cookie_parts']); - $this->assertSame($this->request, $credentials['request']); + $this->rememberMeServices->expects($this->once()) + ->method('autoLogin') + ->with($this->request) + ->willReturn(new RememberMeToken($user = new User('wouter', 'test'), 'main', 'secret')); - $user = $this->createMock(UserInterface::class); - $this->rememberMeServices->expects($this->any()) - ->method('performLogin') - ->with($credentials['cookie_parts'], $credentials['request']) - ->willReturn($user); + $passport = $this->authenticator->authenticate($this->request); - $this->assertSame($user, $this->authenticator->getUser($credentials)); - } - - public function testCredentialsAlwaysValid() - { - $this->assertTrue($this->authenticator->checkCredentials([], $this->createMock(UserInterface::class))); + $this->assertSame($user, $passport->getUser()); } private function generateCookieValue() diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/RemoteUserAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/RemoteUserAuthenticatorTest.php index 80cddd1ddb..f55c72abff 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/RemoteUserAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/RemoteUserAuthenticatorTest.php @@ -14,6 +14,7 @@ namespace Symfony\Component\Security\Http\Tests\Authenticator; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; +use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Authenticator\RemoteUserAuthenticator; @@ -22,7 +23,7 @@ class RemoteUserAuthenticatorTest extends TestCase /** * @dataProvider provideAuthenticators */ - public function testSupport(RemoteUserAuthenticator $authenticator, $parameterName) + public function testSupport(UserProviderInterface $userProvider, RemoteUserAuthenticator $authenticator, $parameterName) { $request = $this->createRequest([$parameterName => 'TheUsername']); @@ -39,20 +40,28 @@ class RemoteUserAuthenticatorTest extends TestCase /** * @dataProvider provideAuthenticators */ - public function testGetCredentials(RemoteUserAuthenticator $authenticator, $parameterName) + public function testAuthenticate(UserProviderInterface $userProvider, RemoteUserAuthenticator $authenticator, $parameterName) { $request = $this->createRequest([$parameterName => 'TheUsername']); $authenticator->supports($request); - $this->assertEquals(['username' => 'TheUsername'], $authenticator->getCredentials($request)); + + $userProvider->expects($this->once()) + ->method('loadUserByUsername') + ->with('TheUsername') + ->willReturn($user = new User('TheUsername', null)); + + $passport = $authenticator->authenticate($request); + $this->assertEquals($user, $passport->getUser()); } public function provideAuthenticators() { $userProvider = $this->createMock(UserProviderInterface::class); + yield [$userProvider, new RemoteUserAuthenticator($userProvider, new TokenStorage(), 'main'), 'REMOTE_USER']; - yield [new RemoteUserAuthenticator($userProvider, new TokenStorage(), 'main'), 'REMOTE_USER']; - yield [new RemoteUserAuthenticator($userProvider, new TokenStorage(), 'main', 'CUSTOM_USER_PARAMETER'), 'CUSTOM_USER_PARAMETER']; + $userProvider = $this->createMock(UserProviderInterface::class); + yield [$userProvider, new RemoteUserAuthenticator($userProvider, new TokenStorage(), 'main', 'CUSTOM_USER_PARAMETER'), 'CUSTOM_USER_PARAMETER']; } private function createRequest(array $server) diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/X509AuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/X509AuthenticatorTest.php index e839504285..2490f9d042 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/X509AuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/X509AuthenticatorTest.php @@ -14,6 +14,7 @@ namespace Symfony\Component\Security\Http\Tests\Authenticator; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; +use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Authenticator\X509Authenticator; @@ -43,7 +44,13 @@ class X509AuthenticatorTest extends TestCase $request = $this->createRequest($serverVars); $this->assertTrue($this->authenticator->supports($request)); - $this->assertEquals(['username' => $user], $this->authenticator->getCredentials($request)); + + $this->userProvider->expects($this->once()) + ->method('loadUserByUsername') + ->with($user) + ->willReturn(new User($user, null)); + + $this->authenticator->authenticate($request); } public static function provideServerVars() @@ -60,7 +67,13 @@ class X509AuthenticatorTest extends TestCase $request = $this->createRequest(['SSL_CLIENT_S_DN' => $credentials]); $this->assertTrue($this->authenticator->supports($request)); - $this->assertEquals(['username' => $emailAddress], $this->authenticator->getCredentials($request)); + + $this->userProvider->expects($this->once()) + ->method('loadUserByUsername') + ->with($emailAddress) + ->willReturn(new User($emailAddress, null)); + + $this->authenticator->authenticate($request); } public static function provideServerVarsNoUser() @@ -89,7 +102,13 @@ class X509AuthenticatorTest extends TestCase 'TheUserKey' => 'TheUser', ]); $this->assertTrue($authenticator->supports($request)); - $this->assertEquals(['username' => 'TheUser'], $authenticator->getCredentials($request)); + + $this->userProvider->expects($this->once()) + ->method('loadUserByUsername') + ->with('TheUser') + ->willReturn(new User('TheUser', null)); + + $authenticator->authenticate($request); } public function testAuthenticationCustomCredentialsKey() @@ -100,7 +119,13 @@ class X509AuthenticatorTest extends TestCase 'TheCertKey' => 'CN=Sample certificate DN/emailAddress=cert@example.com', ]); $this->assertTrue($authenticator->supports($request)); - $this->assertEquals(['username' => 'cert@example.com'], $authenticator->getCredentials($request)); + + $this->userProvider->expects($this->once()) + ->method('loadUserByUsername') + ->with('cert@example.com') + ->willReturn(new User('cert@example.com', null)); + + $authenticator->authenticate($request); } private function createRequest(array $server) diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/CsrfProtectionListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/CsrfProtectionListenerTest.php index 0c2a15d952..baca526bfe 100644 --- a/src/Symfony/Component/Security/Http/Tests/EventListener/CsrfProtectionListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/CsrfProtectionListenerTest.php @@ -13,10 +13,12 @@ namespace Symfony\Component\Security\Http\Tests\EventListener; use PHPUnit\Framework\TestCase; use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; +use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Csrf\CsrfToken; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; -use Symfony\Component\Security\Http\Authenticator\CsrfProtectedAuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; use Symfony\Component\Security\Http\EventListener\CsrfProtectionListener; @@ -31,11 +33,11 @@ class CsrfProtectionListenerTest extends TestCase $this->listener = new CsrfProtectionListener($this->csrfTokenManager); } - public function testNonCsrfProtectedAuthenticator() + public function testNoCsrfTokenBadge() { $this->csrfTokenManager->expects($this->never())->method('isTokenValid'); - $event = $this->createEvent($this->createAuthenticator(false)); + $event = $this->createEvent($this->createPassport(null)); $this->listener->verifyCredentials($event); } @@ -46,7 +48,7 @@ class CsrfProtectionListenerTest extends TestCase ->with(new CsrfToken('authenticator_token_id', 'abc123')) ->willReturn(true); - $event = $this->createEvent($this->createAuthenticator(true), ['_csrf' => 'abc123']); + $event = $this->createEvent($this->createPassport(new CsrfTokenBadge('authenticator_token_id', 'abc123'))); $this->listener->verifyCredentials($event); $this->expectNotToPerformAssertions(); @@ -62,28 +64,22 @@ class CsrfProtectionListenerTest extends TestCase ->with(new CsrfToken('authenticator_token_id', 'abc123')) ->willReturn(false); - $event = $this->createEvent($this->createAuthenticator(true), ['_csrf' => 'abc123']); + $event = $this->createEvent($this->createPassport(new CsrfTokenBadge('authenticator_token_id', 'abc123'))); $this->listener->verifyCredentials($event); } - private function createEvent($authenticator, $credentials = null) + private function createEvent($passport) { - return new VerifyAuthenticatorCredentialsEvent($authenticator, $credentials, null); + return new VerifyAuthenticatorCredentialsEvent($this->createMock(AuthenticatorInterface::class), $passport); } - private function createAuthenticator($supportsCsrf) + private function createPassport(?CsrfTokenBadge $badge) { - if (!$supportsCsrf) { - return $this->createMock(AuthenticatorInterface::class); + $passport = new SelfValidatingPassport(new User('wouter', 'pass')); + if ($badge) { + $passport->addBadge($badge); } - $authenticator = $this->createMock([AuthenticatorInterface::class, CsrfProtectedAuthenticatorInterface::class]); - $authenticator->expects($this->any())->method('getCsrfTokenId')->willReturn('authenticator_token_id'); - $authenticator->expects($this->any()) - ->method('getCsrfToken') - ->with(['_csrf' => 'abc123']) - ->willReturn('abc123'); - - return $authenticator; + return $passport; } } diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/PasswordMigratingListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/PasswordMigratingListenerTest.php index 37d9ee23cc..5b08721e46 100644 --- a/src/Symfony/Component/Security/Http/Tests/EventListener/PasswordMigratingListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/PasswordMigratingListenerTest.php @@ -12,13 +12,17 @@ namespace Symfony\Component\Security\Http\Tests\EventListener; use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface; use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; -use Symfony\Component\Security\Http\Authenticator\PasswordAuthenticatedInterface; -use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; +use Symfony\Component\Security\Http\Event\LoginSuccessEvent; use Symfony\Component\Security\Http\EventListener\PasswordMigratingListener; class PasswordMigratingListenerTest extends TestCase @@ -41,23 +45,19 @@ class PasswordMigratingListenerTest extends TestCase { $this->encoderFactory->expects($this->never())->method('getEncoder'); - $this->listener->onCredentialsVerification($event); + $this->listener->onLoginSuccess($event); } public function provideUnsupportedEvents() { - // unsupported authenticators - yield [$this->createEvent($this->createMock(AuthenticatorInterface::class), $this->user)]; - yield [$this->createEvent($this->createMock([AuthenticatorInterface::class, PasswordAuthenticatedInterface::class]), $this->user)]; + // no password upgrade badge + yield [$this->createEvent(new SelfValidatingPassport($this->createMock(UserInterface::class)))]; - // null password - yield [$this->createEvent($this->createAuthenticator(null), $this->user)]; + // blank password + yield [$this->createEvent(new SelfValidatingPassport($this->createMock(UserInterface::class), [new PasswordUpgradeBadge('', $this->createPasswordUpgrader())]))]; // no user - yield [$this->createEvent($this->createAuthenticator('pa$$word'), null)]; - - // invalid password - yield [$this->createEvent($this->createAuthenticator('pa$$word'), $this->user, false)]; + yield [$this->createEvent($this->createMock(PassportInterface::class))]; } public function testUpgrade() @@ -70,32 +70,23 @@ class PasswordMigratingListenerTest extends TestCase $this->user->expects($this->any())->method('getPassword')->willReturn('old-encoded-password'); - $authenticator = $this->createAuthenticator('pa$$word'); - $authenticator->expects($this->once()) + $passwordUpgrader = $this->createPasswordUpgrader(); + $passwordUpgrader->expects($this->once()) ->method('upgradePassword') ->with($this->user, 'new-encoded-password') ; - $event = $this->createEvent($authenticator, $this->user); - $this->listener->onCredentialsVerification($event); + $event = $this->createEvent(new SelfValidatingPassport($this->user, [new PasswordUpgradeBadge('pa$$word', $passwordUpgrader)])); + $this->listener->onLoginSuccess($event); } - /** - * @return AuthenticatorInterface - */ - private function createAuthenticator($password) + private function createPasswordUpgrader() { - $authenticator = $this->createMock([AuthenticatorInterface::class, PasswordAuthenticatedInterface::class, PasswordUpgraderInterface::class]); - $authenticator->expects($this->any())->method('getPassword')->willReturn($password); - - return $authenticator; + return $this->createMock(PasswordUpgraderInterface::class); } - private function createEvent($authenticator, $user, $credentialsValid = true) + private function createEvent(PassportInterface $passport) { - $event = new VerifyAuthenticatorCredentialsEvent($authenticator, [], $user); - $event->setCredentialsValid($credentialsValid); - - return $event; + return new LoginSuccessEvent($this->createMock(AuthenticatorInterface::class), $passport, $this->createMock(TokenInterface::class), new Request(), null, 'main'); } } diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/RememberMeListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/RememberMeListenerTest.php index 910c67a0bd..9af16a6a76 100644 --- a/src/Symfony/Component/Security/Http/Tests/EventListener/RememberMeListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/RememberMeListenerTest.php @@ -16,8 +16,11 @@ 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\User; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; -use Symfony\Component\Security\Http\Authenticator\RememberMeAuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; use Symfony\Component\Security\Http\Event\LoginFailureEvent; use Symfony\Component\Security\Http\Event\LoginSuccessEvent; use Symfony\Component\Security\Http\EventListener\RememberMeListener; @@ -40,26 +43,14 @@ class RememberMeListenerTest extends TestCase $this->token = $this->createMock(TokenInterface::class); } - /** - * @dataProvider provideUnsupportingAuthenticators - */ - public function testSuccessfulLoginWithoutSupportingAuthenticator($authenticator) + public function testSuccessfulLoginWithoutSupportingAuthenticator() { $this->rememberMeServices->expects($this->never())->method('loginSuccess'); - $event = $this->createLoginSuccessfulEvent('main_firewall', $this->response, $authenticator); + $event = $this->createLoginSuccessfulEvent('main_firewall', $this->response, new SelfValidatingPassport(new User('wouter', null))); $this->listener->onSuccessfulLogin($event); } - public function provideUnsupportingAuthenticators() - { - yield [$this->createMock(AuthenticatorInterface::class)]; - - $authenticator = $this->createMock([AuthenticatorInterface::class, RememberMeAuthenticatorInterface::class]); - $authenticator->expects($this->any())->method('supportsRememberMe')->willReturn(false); - yield [$authenticator]; - } - public function testSuccessfulLoginWithoutSuccessResponse() { $this->rememberMeServices->expects($this->never())->method('loginSuccess'); @@ -84,14 +75,13 @@ class RememberMeListenerTest extends TestCase $this->listener->onFailedLogin($event); } - private function createLoginSuccessfulEvent($providerKey, $response, $authenticator = null) + private function createLoginSuccessfulEvent($providerKey, $response, PassportInterface $passport = null) { - if (null === $authenticator) { - $authenticator = $this->createMock([AuthenticatorInterface::class, RememberMeAuthenticatorInterface::class]); - $authenticator->expects($this->any())->method('supportsRememberMe')->willReturn(true); + if (null === $passport) { + $passport = new SelfValidatingPassport(new User('test', null), [new RememberMeBadge()]); } - return new LoginSuccessEvent($authenticator, $this->token, $this->request, $response, $providerKey); + return new LoginSuccessEvent($this->createMock(AuthenticatorInterface::class), $passport, $this->token, $this->request, $response, $providerKey); } private function createLoginFailureEvent($providerKey) diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/SessionListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/SessionStrategyListenerTest.php similarity index 89% rename from src/Symfony/Component/Security/Http/Tests/EventListener/SessionListenerTest.php rename to src/Symfony/Component/Security/Http/Tests/EventListener/SessionStrategyListenerTest.php index 176921d1a1..4d1dd0a5be 100644 --- a/src/Symfony/Component/Security/Http/Tests/EventListener/SessionListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/SessionStrategyListenerTest.php @@ -14,12 +14,14 @@ namespace Symfony\Component\Security\Http\Tests\EventListener; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; use Symfony\Component\Security\Http\Event\LoginSuccessEvent; use Symfony\Component\Security\Http\EventListener\SessionStrategyListener; use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface; -class SessionListenerTest extends TestCase +class SessionStrategyListenerTest extends TestCase { private $sessionAuthenticationStrategy; private $listener; @@ -60,7 +62,7 @@ class SessionListenerTest extends TestCase private function createEvent($providerKey) { - return new LoginSuccessEvent($this->createMock(AuthenticatorInterface::class), $this->token, $this->request, null, $providerKey); + return new LoginSuccessEvent($this->createMock(AuthenticatorInterface::class), new SelfValidatingPassport(new User('test', null)), $this->token, $this->request, null, $providerKey); } private function configurePreviousSession() diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/UserCheckerListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/UserCheckerListenerTest.php index 785a312963..dac1fbaf92 100644 --- a/src/Symfony/Component/Security/Http/Tests/EventListener/UserCheckerListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/UserCheckerListenerTest.php @@ -12,9 +12,15 @@ namespace Symfony\Component\Security\Http\Tests\EventListener; use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Core\User\UserCheckerInterface; -use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PreAuthenticatedUserBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; +use Symfony\Component\Security\Http\Event\LoginSuccessEvent; use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; use Symfony\Component\Security\Http\EventListener\UserCheckerListener; @@ -28,51 +34,59 @@ class UserCheckerListenerTest extends TestCase { $this->userChecker = $this->createMock(UserCheckerInterface::class); $this->listener = new UserCheckerListener($this->userChecker); - $this->user = $this->createMock(UserInterface::class); + $this->user = new User('test', null); } public function testPreAuth() { $this->userChecker->expects($this->once())->method('checkPreAuth')->with($this->user); - $this->listener->preCredentialsVerification($this->createEvent()); + $this->listener->preCredentialsVerification($this->createVerifyAuthenticatorCredentialsEvent()); } public function testPreAuthNoUser() { $this->userChecker->expects($this->never())->method('checkPreAuth'); - $this->listener->preCredentialsVerification($this->createEvent(true, null)); + $this->listener->preCredentialsVerification($this->createVerifyAuthenticatorCredentialsEvent($this->createMock(PassportInterface::class))); + } + + public function testPreAuthenticatedBadge() + { + $this->userChecker->expects($this->never())->method('checkPreAuth'); + + $this->listener->preCredentialsVerification($this->createVerifyAuthenticatorCredentialsEvent(new SelfValidatingPassport($this->user, [new PreAuthenticatedUserBadge()]))); } public function testPostAuthValidCredentials() { $this->userChecker->expects($this->once())->method('checkPostAuth')->with($this->user); - $this->listener->postCredentialsVerification($this->createEvent(true)); - } - - public function testPostAuthInvalidCredentials() - { - $this->userChecker->expects($this->never())->method('checkPostAuth')->with($this->user); - - $this->listener->postCredentialsVerification($this->createEvent()); + $this->listener->postCredentialsVerification($this->createLoginSuccessEvent()); } public function testPostAuthNoUser() { $this->userChecker->expects($this->never())->method('checkPostAuth'); - $this->listener->postCredentialsVerification($this->createEvent(true, null)); + $this->listener->postCredentialsVerification($this->createLoginSuccessEvent($this->createMock(PassportInterface::class))); } - private function createEvent($credentialsValid = false, $customUser = false) + private function createVerifyAuthenticatorCredentialsEvent($passport = null) { - $event = new VerifyAuthenticatorCredentialsEvent($this->createMock(AuthenticatorInterface::class), [], false === $customUser ? $this->user : $customUser); - if ($credentialsValid) { - $event->setCredentialsValid(true); + if (null === $passport) { + $passport = new SelfValidatingPassport($this->user); } - return $event; + return new VerifyAuthenticatorCredentialsEvent($this->createMock(AuthenticatorInterface::class), $passport); + } + + private function createLoginSuccessEvent($passport = null) + { + if (null === $passport) { + $passport = new SelfValidatingPassport($this->user); + } + + return new LoginSuccessEvent($this->createMock(AuthenticatorInterface::class), $passport, $this->createMock(TokenInterface::class), new Request(), null, 'main'); } } diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/VerifyAuthenticatorCredentialsListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/VerifyAuthenticatorCredentialsListenerTest.php index e2c2cc6605..a4850ebda7 100644 --- a/src/Symfony/Component/Security/Http/Tests/EventListener/VerifyAuthenticatorCredentialsListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/VerifyAuthenticatorCredentialsListenerTest.php @@ -15,12 +15,12 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface; use Symfony\Component\Security\Core\Exception\BadCredentialsException; -use Symfony\Component\Security\Core\Exception\LogicException; -use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; -use Symfony\Component\Security\Http\Authenticator\CustomAuthenticatedInterface; -use Symfony\Component\Security\Http\Authenticator\PasswordAuthenticatedInterface; -use Symfony\Component\Security\Http\Authenticator\TokenAuthenticatedInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials; +use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; +use Symfony\Component\Security\Http\Authenticator\Passport\Passport; +use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; use Symfony\Component\Security\Http\EventListener\VerifyAuthenticatorCredentialsListener; @@ -34,7 +34,7 @@ class VerifyAuthenticatorCredentialsListenerTest extends TestCase { $this->encoderFactory = $this->createMock(EncoderFactoryInterface::class); $this->listener = new VerifyAuthenticatorCredentialsListener($this->encoderFactory); - $this->user = $this->createMock(UserInterface::class); + $this->user = new User('wouter', 'encoded-password'); } /** @@ -42,16 +42,22 @@ class VerifyAuthenticatorCredentialsListenerTest extends TestCase */ public function testPasswordAuthenticated($password, $passwordValid, $result) { - $this->user->expects($this->any())->method('getPassword')->willReturn('encoded-password'); - $encoder = $this->createMock(PasswordEncoderInterface::class); $encoder->expects($this->any())->method('isPasswordValid')->with('encoded-password', $password)->willReturn($passwordValid); $this->encoderFactory->expects($this->any())->method('getEncoder')->with($this->identicalTo($this->user))->willReturn($encoder); - $event = new VerifyAuthenticatorCredentialsEvent($this->createAuthenticator('password', $password), ['password' => $password], $this->user); - $this->listener->onAuthenticating($event); - $this->assertEquals($result, $event->areCredentialsValid()); + if (false === $result) { + $this->expectException(BadCredentialsException::class); + $this->expectExceptionMessage('The presented password is invalid.'); + } + + $credentials = new PasswordCredentials($password); + $this->listener->onAuthenticating($this->createEvent(new Passport($this->user, $credentials))); + + if (true === $result) { + $this->assertTrue($credentials->isResolved()); + } } public function providePasswords() @@ -67,30 +73,10 @@ class VerifyAuthenticatorCredentialsListenerTest extends TestCase $this->encoderFactory->expects($this->never())->method('getEncoder'); - $event = new VerifyAuthenticatorCredentialsEvent($this->createAuthenticator('password', ''), ['password' => ''], $this->user); + $event = $this->createEvent(new Passport($this->user, new PasswordCredentials(''))); $this->listener->onAuthenticating($event); } - public function testTokenAuthenticated() - { - $this->encoderFactory->expects($this->never())->method('getEncoder'); - - $event = new VerifyAuthenticatorCredentialsEvent($this->createAuthenticator('token', 'some_token'), ['token' => 'abc'], $this->user); - $this->listener->onAuthenticating($event); - - $this->assertTrue($event->areCredentialsValid()); - } - - public function testTokenAuthenticatedReturningNull() - { - $this->encoderFactory->expects($this->never())->method('getEncoder'); - - $event = new VerifyAuthenticatorCredentialsEvent($this->createAuthenticator('token', null), ['token' => 'abc'], $this->user); - $this->listener->onAuthenticating($event); - - $this->assertFalse($event->areCredentialsValid()); - } - /** * @dataProvider provideCustomAuthenticatedResults */ @@ -98,10 +84,18 @@ class VerifyAuthenticatorCredentialsListenerTest extends TestCase { $this->encoderFactory->expects($this->never())->method('getEncoder'); - $event = new VerifyAuthenticatorCredentialsEvent($this->createAuthenticator('custom', $result), [], $this->user); - $this->listener->onAuthenticating($event); + if (false === $result) { + $this->expectException(BadCredentialsException::class); + } - $this->assertEquals($result, $event->areCredentialsValid()); + $credentials = new CustomCredentials(function () use ($result) { + return $result; + }, ['password' => 'foo']); + $this->listener->onAuthenticating($this->createEvent(new Passport($this->user, $credentials))); + + if (true === $result) { + $this->assertTrue($credentials->isResolved()); + } } public function provideCustomAuthenticatedResults() @@ -110,58 +104,16 @@ class VerifyAuthenticatorCredentialsListenerTest extends TestCase yield [false]; } - public function testAlreadyAuthenticated() + public function testNoCredentialsBadgeProvided() { - $event = new VerifyAuthenticatorCredentialsEvent($this->createAuthenticator(), [], $this->user); - $event->setCredentialsValid(true); - $this->listener->onAuthenticating($event); - - $this->assertTrue($event->areCredentialsValid()); - } - - public function testNoAuthenticatedInterfaceImplemented() - { - $authenticator = $this->createAuthenticator(); - $this->expectException(LogicException::class); - $this->expectExceptionMessage(sprintf('Authenticator %s does not have valid credentials. Authenticators must implement one of the authenticated interfaces (%s, %s or %s).', \get_class($authenticator), PasswordAuthenticatedInterface::class, TokenAuthenticatedInterface::class, CustomAuthenticatedInterface::class)); - $this->encoderFactory->expects($this->never())->method('getEncoder'); - $event = new VerifyAuthenticatorCredentialsEvent($authenticator, [], $this->user); + $event = $this->createEvent(new SelfValidatingPassport($this->user)); $this->listener->onAuthenticating($event); } - /** - * @return AuthenticatorInterface - */ - private function createAuthenticator(?string $type = null, $result = null) + private function createEvent($passport) { - $interfaces = [AuthenticatorInterface::class]; - switch ($type) { - case 'password': - $interfaces[] = PasswordAuthenticatedInterface::class; - break; - case 'token': - $interfaces[] = TokenAuthenticatedInterface::class; - break; - case 'custom': - $interfaces[] = CustomAuthenticatedInterface::class; - break; - } - - $authenticator = $this->createMock(1 === \count($interfaces) ? $interfaces[0] : $interfaces); - switch ($type) { - case 'password': - $authenticator->expects($this->any())->method('getPassword')->willReturn($result); - break; - case 'token': - $authenticator->expects($this->any())->method('getToken')->willReturn($result); - break; - case 'custom': - $authenticator->expects($this->any())->method('checkCredentials')->willReturn($result); - break; - } - - return $authenticator; + return new VerifyAuthenticatorCredentialsEvent($this->createMock(AuthenticatorInterface::class), $passport); } }