Added tests
This commit is contained in:
parent
59f49b20ca
commit
6b9d78d5e0
@ -104,9 +104,8 @@ class FormLoginFactory extends AbstractFactory implements AuthenticatorFactoryIn
|
||||
$options = array_merge($defaultOptions, array_intersect_key($config, $defaultOptions));
|
||||
$container
|
||||
->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.form_login'))
|
||||
->replaceArgument(1, isset($config['csrf_token_generator']) ? new Reference($config['csrf_token_generator']) : null)
|
||||
->replaceArgument(2, new Reference($userProviderId))
|
||||
->replaceArgument(3, $options);
|
||||
->replaceArgument(1, new Reference($userProviderId))
|
||||
->replaceArgument(2, $options);
|
||||
|
||||
return $authenticatorId;
|
||||
}
|
||||
|
@ -84,7 +84,6 @@
|
||||
abstract="true">
|
||||
<argument type="abstract">realm name</argument>
|
||||
<argument type="abstract">user provider</argument>
|
||||
<argument type="service" id="security.encoder_factory" />
|
||||
<argument type="service" id="logger" on-invalid="null" />
|
||||
</service>
|
||||
|
||||
@ -92,7 +91,6 @@
|
||||
class="Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator"
|
||||
abstract="true">
|
||||
<argument type="service" id="security.http_utils" />
|
||||
<argument /> <!-- csrf token generator -->
|
||||
<argument type="abstract">user provider</argument>
|
||||
<argument type="abstract">options</argument>
|
||||
</service>
|
||||
|
@ -23,7 +23,6 @@ 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\Token\PreAuthenticationToken;
|
||||
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
|
||||
use Symfony\Component\Security\Http\Event\LoginFailureEvent;
|
||||
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
|
||||
@ -40,8 +39,6 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
*/
|
||||
class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthenticatorInterface
|
||||
{
|
||||
use AuthenticatorManagerTrait;
|
||||
|
||||
private $authenticators;
|
||||
private $tokenStorage;
|
||||
private $eventDispatcher;
|
||||
@ -131,7 +128,9 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent
|
||||
// lazily (after initialization). This is important for e.g. the AnonymousAuthenticator
|
||||
// as its support is relying on the (initialized) token in the TokenStorage.
|
||||
if (false === $authenticator->supports($request)) {
|
||||
$this->logger->debug('Skipping the "{authenticator}" authenticator as it did not support the request.', ['authenticator' => \get_class($authenticator)]);
|
||||
if (null !== $this->logger) {
|
||||
$this->logger->debug('Skipping the "{authenticator}" authenticator as it did not support the request.', ['authenticator' => \get_class($authenticator)]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -215,21 +214,14 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent
|
||||
throw new UsernameNotFoundException(sprintf('Null returned from "%s::getUser()".', \get_class($authenticator)));
|
||||
}
|
||||
|
||||
if (!$user instanceof UserInterface) {
|
||||
throw new \UnexpectedValueException(sprintf('The %s::getUser() method must return a UserInterface. You returned %s.', \get_class($authenticator), \is_object($user) ? \get_class($user) : \gettype($user)));
|
||||
}
|
||||
|
||||
$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
|
||||
// turn the UserInterface into a TokenInterface
|
||||
$authenticatedToken = $authenticator->createAuthenticatedToken($user, $this->providerKey);
|
||||
if (!$authenticatedToken instanceof TokenInterface) {
|
||||
throw new \UnexpectedValueException(sprintf('The %s::createAuthenticatedToken() method must return a TokenInterface. You returned %s.', \get_class($authenticator), \is_object($authenticatedToken) ? \get_class($authenticatedToken) : \gettype($authenticatedToken)));
|
||||
}
|
||||
|
||||
if (true === $this->eraseCredentials) {
|
||||
$authenticatedToken->eraseCredentials();
|
||||
@ -259,21 +251,10 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent
|
||||
return $loginSuccessEvent->getResponse();
|
||||
}
|
||||
|
||||
private function handleAuthenticationFailure(AuthenticationException $exception, TokenInterface $token)
|
||||
{
|
||||
if (null !== $this->eventDispatcher) {
|
||||
$this->eventDispatcher->dispatch(new AuthenticationFailureEvent($token, $exception), AuthenticationEvents::AUTHENTICATION_FAILURE);
|
||||
}
|
||||
|
||||
$exception->setToken($token);
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles an authentication failure and returns the Response for the authenticator.
|
||||
*/
|
||||
private function handleAuthenticatorFailure(AuthenticationException $authenticationException, Request $request, AuthenticatorInterface $authenticator): ?Response
|
||||
private function handleAuthenticationFailure(AuthenticationException $authenticationException, Request $request, AuthenticatorInterface $authenticator): ?Response
|
||||
{
|
||||
$response = $authenticator->onAuthenticationFailure($request, $authenticationException);
|
||||
|
||||
|
@ -20,7 +20,6 @@ use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
use Symfony\Component\Security\Http\HttpUtils;
|
||||
use Symfony\Component\Security\Http\ParameterBagUtils;
|
||||
use Symfony\Component\Security\Http\Util\TargetPathTrait;
|
||||
@ -38,13 +37,11 @@ class FormLoginAuthenticator extends AbstractLoginFormAuthenticator implements P
|
||||
|
||||
private $options;
|
||||
private $httpUtils;
|
||||
private $csrfTokenManager;
|
||||
private $userProvider;
|
||||
|
||||
public function __construct(HttpUtils $httpUtils, ?CsrfTokenManagerInterface $csrfTokenManager, UserProviderInterface $userProvider, array $options)
|
||||
public function __construct(HttpUtils $httpUtils, UserProviderInterface $userProvider, array $options)
|
||||
{
|
||||
$this->httpUtils = $httpUtils;
|
||||
$this->csrfTokenManager = $csrfTokenManager;
|
||||
$this->options = array_merge([
|
||||
'username_parameter' => '_username',
|
||||
'password_parameter' => '_password',
|
||||
@ -75,10 +72,7 @@ class FormLoginAuthenticator extends AbstractLoginFormAuthenticator implements P
|
||||
public function getCredentials(Request $request): array
|
||||
{
|
||||
$credentials = [];
|
||||
|
||||
if (null !== $this->csrfTokenManager) {
|
||||
$credentials['csrf_token'] = ParameterBagUtils::getRequestParameterValue($request, $this->options['csrf_parameter']);
|
||||
}
|
||||
$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']);
|
||||
|
@ -16,7 +16,6 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
@ -33,14 +32,12 @@ class HttpBasicAuthenticator implements AuthenticatorInterface, AuthenticationEn
|
||||
{
|
||||
private $realmName;
|
||||
private $userProvider;
|
||||
private $encoderFactory;
|
||||
private $logger;
|
||||
|
||||
public function __construct(string $realmName, UserProviderInterface $userProvider, EncoderFactoryInterface $encoderFactory, ?LoggerInterface $logger = null)
|
||||
public function __construct(string $realmName, UserProviderInterface $userProvider, ?LoggerInterface $logger = null)
|
||||
{
|
||||
$this->realmName = $realmName;
|
||||
$this->userProvider = $userProvider;
|
||||
$this->encoderFactory = $encoderFactory;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Authenticator\Token;
|
||||
namespace Symfony\Component\Security\Http\Authenticator;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@ -18,9 +18,7 @@ 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\User\UserInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
|
||||
use Symfony\Component\Security\Http\RememberMe\AbstractRememberMeServices;
|
||||
use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy;
|
||||
|
||||
/**
|
||||
* The RememberMe *Authenticator* performs remember me authentication.
|
||||
@ -35,21 +33,22 @@ use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy;
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class RememberMeAuthenticator implements AuthenticatorInterface
|
||||
class RememberMeAuthenticator implements AuthenticatorInterface, CustomAuthenticatedInterface
|
||||
{
|
||||
private $rememberMeServices;
|
||||
private $secret;
|
||||
private $tokenStorage;
|
||||
private $options;
|
||||
private $sessionStrategy;
|
||||
private $options = [
|
||||
'secure' => false,
|
||||
'httponly' => true,
|
||||
];
|
||||
|
||||
public function __construct(AbstractRememberMeServices $rememberMeServices, string $secret, TokenStorageInterface $tokenStorage, array $options, ?SessionAuthenticationStrategy $sessionStrategy = null)
|
||||
public function __construct(AbstractRememberMeServices $rememberMeServices, string $secret, TokenStorageInterface $tokenStorage, array $options)
|
||||
{
|
||||
$this->rememberMeServices = $rememberMeServices;
|
||||
$this->secret = $secret;
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
$this->options = $options;
|
||||
$this->sessionStrategy = $sessionStrategy;
|
||||
$this->options = array_merge($this->options, $options);
|
||||
}
|
||||
|
||||
public function supports(Request $request): ?bool
|
||||
@ -87,6 +86,12 @@ class RememberMeAuthenticator implements AuthenticatorInterface
|
||||
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);
|
||||
@ -101,10 +106,6 @@ class RememberMeAuthenticator implements AuthenticatorInterface
|
||||
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response
|
||||
{
|
||||
if ($request->hasSession() && $request->getSession()->isStarted()) {
|
||||
$this->sessionStrategy->onAuthentication($request, $token);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ class PasswordMigratingListener implements EventSubscriberInterface
|
||||
return;
|
||||
}
|
||||
|
||||
if (null !== $password = $authenticator->getPassword($event->getCredentials())) {
|
||||
if (null === $password = $authenticator->getPassword($event->getCredentials())) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -46,11 +46,11 @@ class PasswordMigratingListener implements EventSubscriberInterface
|
||||
}
|
||||
|
||||
$passwordEncoder = $this->encoderFactory->getEncoder($user);
|
||||
if (!method_exists($passwordEncoder, 'needsRehash') || !$passwordEncoder->needsRehash($user)) {
|
||||
if (!$passwordEncoder->needsRehash($user->getPassword())) {
|
||||
return;
|
||||
}
|
||||
|
||||
$authenticator->upgradePassword($user, $passwordEncoder->encodePassword($user, $password));
|
||||
$authenticator->upgradePassword($user, $passwordEncoder->encodePassword($password, $user->getSalt()));
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
|
@ -39,7 +39,15 @@ class RememberMeListener implements EventSubscriberInterface
|
||||
|
||||
public function onSuccessfulLogin(LoginSuccessEvent $event): void
|
||||
{
|
||||
if (!$this->isRememberMeEnabled($event->getAuthenticator(), $event->getProviderKey())) {
|
||||
if (!$this->isRememberMeEnabled($event->getProviderKey(), $event->getAuthenticator())) {
|
||||
return;
|
||||
}
|
||||
|
||||
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($event->getAuthenticator())]);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@ -48,21 +56,21 @@ class RememberMeListener implements EventSubscriberInterface
|
||||
|
||||
public function onFailedLogin(LoginFailureEvent $event): void
|
||||
{
|
||||
if (!$this->isRememberMeEnabled($event->getAuthenticator(), $event->getProviderKey())) {
|
||||
if (!$this->isRememberMeEnabled($event->getProviderKey())) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->rememberMeServices->loginFail($event->getRequest(), $event->getException());
|
||||
}
|
||||
|
||||
private function isRememberMeEnabled(AuthenticatorInterface $authenticator, string $providerKey): bool
|
||||
private function isRememberMeEnabled(string $providerKey, ?AuthenticatorInterface $authenticator = null): bool
|
||||
{
|
||||
if ($providerKey !== $this->providerKey) {
|
||||
// This listener is created for a different firewall.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$authenticator instanceof RememberMeAuthenticatorInterface || !$authenticator->supportsRememberMe()) {
|
||||
if (null !== $authenticator && (!$authenticator instanceof RememberMeAuthenticatorInterface || !$authenticator->supportsRememberMe())) {
|
||||
if (null !== $this->logger) {
|
||||
$this->logger->debug('Remember me skipped: your authenticator does not support it.', ['authenticator' => \get_class($authenticator)]);
|
||||
}
|
||||
|
@ -23,11 +23,19 @@ class UserCheckerListener implements EventSubscriberInterface
|
||||
|
||||
public function preCredentialsVerification(VerifyAuthenticatorCredentialsEvent $event): void
|
||||
{
|
||||
if (null === $event->getUser()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->userChecker->checkPreAuth($event->getUser());
|
||||
}
|
||||
|
||||
public function postCredentialsVerification(VerifyAuthenticatorCredentialsEvent $event): void
|
||||
{
|
||||
if (null === $event->getUser() || !$event->areCredentialsValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->userChecker->checkPostAuth($event->getUser());
|
||||
}
|
||||
|
||||
|
@ -31,6 +31,10 @@ class VerifyAuthenticatorCredentialsListener implements EventSubscriberInterface
|
||||
|
||||
public function onAuthenticating(VerifyAuthenticatorCredentialsEvent $event): void
|
||||
{
|
||||
if ($event->areCredentialsValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$authenticator = $event->getAuthenticator();
|
||||
if ($authenticator instanceof PasswordAuthenticatedInterface) {
|
||||
// Use the password encoder to validate the credentials
|
||||
|
@ -0,0 +1,225 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Tests\Authentication;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;
|
||||
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\Authentication\AuthenticatorManager;
|
||||
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
|
||||
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
|
||||
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
|
||||
use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class AuthenticatorManagerTest extends TestCase
|
||||
{
|
||||
private $tokenStorage;
|
||||
private $eventDispatcher;
|
||||
private $request;
|
||||
private $user;
|
||||
private $token;
|
||||
private $response;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->tokenStorage = $this->createMock(TokenStorageInterface::class);
|
||||
$this->eventDispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$this->request = new Request();
|
||||
$this->user = $this->createMock(UserInterface::class);
|
||||
$this->token = $this->createMock(TokenInterface::class);
|
||||
$this->response = $this->createMock(Response::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideSupportsData
|
||||
*/
|
||||
public function testSupports($authenticators, $result)
|
||||
{
|
||||
$manager = $this->createManager($authenticators);
|
||||
|
||||
$this->assertEquals($result, $manager->supports($this->request));
|
||||
}
|
||||
|
||||
public function provideSupportsData()
|
||||
{
|
||||
yield [[$this->createAuthenticator(null), $this->createAuthenticator(null)], null];
|
||||
yield [[$this->createAuthenticator(null), $this->createAuthenticator(false)], null];
|
||||
|
||||
yield [[$this->createAuthenticator(null), $this->createAuthenticator(true)], true];
|
||||
yield [[$this->createAuthenticator(true), $this->createAuthenticator(false)], true];
|
||||
|
||||
yield [[$this->createAuthenticator(false), $this->createAuthenticator(false)], false];
|
||||
yield [[], false];
|
||||
}
|
||||
|
||||
public function testSupportCheckedUponRequestAuthentication()
|
||||
{
|
||||
// the attribute stores the supported authenticators, returning false now
|
||||
// means support changed between calling supports() and authenticateRequest()
|
||||
// (which is the case with lazy firewalls and e.g. the AnonymousAuthenticator)
|
||||
$authenticator = $this->createAuthenticator(false);
|
||||
$this->request->attributes->set('_guard_authenticators', [$authenticator]);
|
||||
|
||||
$authenticator->expects($this->never())->method('getCredentials');
|
||||
|
||||
$manager = $this->createManager([$authenticator]);
|
||||
$manager->authenticateRequest($this->request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideMatchingAuthenticatorIndex
|
||||
*/
|
||||
public function testAuthenticateRequest($matchingAuthenticatorIndex)
|
||||
{
|
||||
$authenticators = [$this->createAuthenticator(0 === $matchingAuthenticatorIndex), $this->createAuthenticator(1 === $matchingAuthenticatorIndex)];
|
||||
$this->request->attributes->set('_guard_authenticators', $authenticators);
|
||||
$matchingAuthenticator = $authenticators[$matchingAuthenticatorIndex];
|
||||
|
||||
$authenticators[($matchingAuthenticatorIndex + 1) % 2]->expects($this->never())->method('getCredentials');
|
||||
|
||||
$matchingAuthenticator->expects($this->any())->method('getCredentials')->willReturn(['password' => 'pa$$']);
|
||||
$matchingAuthenticator->expects($this->any())->method('getUser')->willReturn($this->user);
|
||||
$this->eventDispatcher->expects($this->exactly(4))
|
||||
->method('dispatch')
|
||||
->with($this->callback(function ($event) use ($matchingAuthenticator) {
|
||||
if ($event instanceof VerifyAuthenticatorCredentialsEvent) {
|
||||
return $event->getAuthenticator() === $matchingAuthenticator
|
||||
&& $event->getCredentials() === ['password' => 'pa$$']
|
||||
&& $event->getUser() === $this->user;
|
||||
}
|
||||
|
||||
return $event instanceof InteractiveLoginEvent || $event instanceof LoginSuccessEvent || $event instanceof AuthenticationSuccessEvent;
|
||||
}))
|
||||
->will($this->returnCallback(function ($event) {
|
||||
if ($event instanceof VerifyAuthenticatorCredentialsEvent) {
|
||||
$event->setCredentialsValid(true);
|
||||
}
|
||||
|
||||
return $event;
|
||||
}));
|
||||
$matchingAuthenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token);
|
||||
|
||||
$this->tokenStorage->expects($this->once())->method('setToken')->with($this->token);
|
||||
|
||||
$matchingAuthenticator->expects($this->any())
|
||||
->method('onAuthenticationSuccess')
|
||||
->with($this->anything(), $this->token, 'main')
|
||||
->willReturn($this->response);
|
||||
|
||||
$manager = $this->createManager($authenticators);
|
||||
$this->assertSame($this->response, $manager->authenticateRequest($this->request));
|
||||
}
|
||||
|
||||
public function provideMatchingAuthenticatorIndex()
|
||||
{
|
||||
yield [0];
|
||||
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->once())
|
||||
->method('onAuthenticationFailure')
|
||||
->with($this->request, $this->isInstanceOf(BadCredentialsException::class));
|
||||
|
||||
$manager = $this->createManager([$authenticator]);
|
||||
$manager->authenticateRequest($this->request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideEraseCredentialsData
|
||||
*/
|
||||
public function testEraseCredentials($eraseCredentials)
|
||||
{
|
||||
$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->expects($this->any())
|
||||
->method('dispatch')
|
||||
->will($this->returnCallback(function ($event) {
|
||||
if ($event instanceof VerifyAuthenticatorCredentialsEvent) {
|
||||
$event->setCredentialsValid(true);
|
||||
}
|
||||
|
||||
return $event;
|
||||
}));
|
||||
|
||||
$authenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token);
|
||||
|
||||
$this->token->expects($eraseCredentials ? $this->once() : $this->never())->method('eraseCredentials');
|
||||
|
||||
$manager = $this->createManager([$authenticator], 'main', $eraseCredentials);
|
||||
$manager->authenticateRequest($this->request);
|
||||
}
|
||||
|
||||
public function provideEraseCredentialsData()
|
||||
{
|
||||
yield [true];
|
||||
yield [false];
|
||||
}
|
||||
|
||||
public function testAuthenticateUser()
|
||||
{
|
||||
$authenticator = $this->createAuthenticator();
|
||||
$authenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token);
|
||||
$authenticator->expects($this->any())->method('onAuthenticationSuccess')->willReturn($this->response);
|
||||
|
||||
$this->tokenStorage->expects($this->once())->method('setToken')->with($this->token);
|
||||
|
||||
$manager = $this->createManager([$authenticator]);
|
||||
$this->assertSame($this->response, $manager->authenticateUser($this->user, $authenticator, $this->request));
|
||||
}
|
||||
|
||||
private function createAuthenticator($supports = true)
|
||||
{
|
||||
$authenticator = $this->createMock(AuthenticatorInterface::class);
|
||||
$authenticator->expects($this->any())->method('supports')->willReturn($supports);
|
||||
|
||||
return $authenticator;
|
||||
}
|
||||
|
||||
private function createManager($authenticators, $providerKey = 'main', $eraseCredentials = true)
|
||||
{
|
||||
return new AuthenticatorManager($authenticators, $this->tokenStorage, $this->eventDispatcher, $providerKey, null, $eraseCredentials);
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\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
|
||||
{
|
||||
private $tokenStorage;
|
||||
private $authenticator;
|
||||
private $request;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->tokenStorage = $this->createMock(TokenStorageInterface::class);
|
||||
$this->authenticator = new AnonymousAuthenticator('s3cr3t', $this->tokenStorage);
|
||||
$this->request = new Request();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideSupportsData
|
||||
*/
|
||||
public function testSupports($tokenAlreadyAvailable, $result)
|
||||
{
|
||||
$this->tokenStorage->expects($this->any())->method('getToken')->willReturn($tokenAlreadyAvailable ? $this->createMock(TokenStorageInterface::class) : null);
|
||||
|
||||
$this->assertEquals($result, $this->authenticator->supports($this->request));
|
||||
}
|
||||
|
||||
public function provideSupportsData()
|
||||
{
|
||||
yield [true, null];
|
||||
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');
|
||||
|
||||
$this->assertTrue($token->isAuthenticated());
|
||||
$this->assertEquals('anon.', $token->getUser());
|
||||
}
|
||||
}
|
@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Tests\Authenticator;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
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\UserProviderInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator;
|
||||
use Symfony\Component\Security\Http\HttpUtils;
|
||||
|
||||
class FormLoginAuthenticatorTest extends TestCase
|
||||
{
|
||||
private $userProvider;
|
||||
private $authenticator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->userProvider = $this->createMock(UserProviderInterface::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideUsernamesForLength
|
||||
*/
|
||||
public function testHandleWhenUsernameLength($username, $ok)
|
||||
{
|
||||
if ($ok) {
|
||||
$this->expectNotToPerformAssertions();
|
||||
} else {
|
||||
$this->expectException(BadCredentialsException::class);
|
||||
$this->expectExceptionMessage('Invalid username.');
|
||||
}
|
||||
|
||||
$request = Request::create('/login_check', 'POST', ['_username' => $username]);
|
||||
$request->setSession($this->createSession());
|
||||
|
||||
$this->setUpAuthenticator();
|
||||
$this->authenticator->getCredentials($request);
|
||||
}
|
||||
|
||||
public function provideUsernamesForLength()
|
||||
{
|
||||
yield [str_repeat('x', Security::MAX_USERNAME_LENGTH + 1), false];
|
||||
yield [str_repeat('x', Security::MAX_USERNAME_LENGTH - 1), true];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider postOnlyDataProvider
|
||||
*/
|
||||
public function testHandleNonStringUsernameWithArray($postOnly)
|
||||
{
|
||||
$this->expectException(BadRequestHttpException::class);
|
||||
$this->expectExceptionMessage('The key "_username" must be a string, "array" given.');
|
||||
|
||||
$request = Request::create('/login_check', 'POST', ['_username' => []]);
|
||||
$request->setSession($this->createSession());
|
||||
|
||||
$this->setUpAuthenticator(['post_only' => $postOnly]);
|
||||
$this->authenticator->getCredentials($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider postOnlyDataProvider
|
||||
*/
|
||||
public function testHandleNonStringUsernameWithInt($postOnly)
|
||||
{
|
||||
$this->expectException(BadRequestHttpException::class);
|
||||
$this->expectExceptionMessage('The key "_username" must be a string, "integer" given.');
|
||||
|
||||
$request = Request::create('/login_check', 'POST', ['_username' => 42]);
|
||||
$request->setSession($this->createSession());
|
||||
|
||||
$this->setUpAuthenticator(['post_only' => $postOnly]);
|
||||
$this->authenticator->getCredentials($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider postOnlyDataProvider
|
||||
*/
|
||||
public function testHandleNonStringUsernameWithObject($postOnly)
|
||||
{
|
||||
$this->expectException(BadRequestHttpException::class);
|
||||
$this->expectExceptionMessage('The key "_username" must be a string, "object" given.');
|
||||
|
||||
$request = Request::create('/login_check', 'POST', ['_username' => new \stdClass()]);
|
||||
$request->setSession($this->createSession());
|
||||
|
||||
$this->setUpAuthenticator(['post_only' => $postOnly]);
|
||||
$this->authenticator->getCredentials($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider postOnlyDataProvider
|
||||
*/
|
||||
public function testHandleNonStringUsernameWith__toString($postOnly)
|
||||
{
|
||||
$usernameObject = $this->getMockBuilder(DummyUserClass::class)->getMock();
|
||||
$usernameObject->expects($this->once())->method('__toString')->willReturn('someUsername');
|
||||
|
||||
$request = Request::create('/login_check', 'POST', ['_username' => $usernameObject]);
|
||||
$request->setSession($this->createSession());
|
||||
|
||||
$this->setUpAuthenticator(['post_only' => $postOnly]);
|
||||
$this->authenticator->getCredentials($request);
|
||||
}
|
||||
|
||||
public function postOnlyDataProvider()
|
||||
{
|
||||
yield [true];
|
||||
yield [false];
|
||||
}
|
||||
|
||||
private function setUpAuthenticator(array $options = [])
|
||||
{
|
||||
$this->authenticator = new FormLoginAuthenticator(new HttpUtils(), $this->userProvider, $options);
|
||||
}
|
||||
|
||||
private function createSession()
|
||||
{
|
||||
return $this->createMock('Symfony\Component\HttpFoundation\Session\SessionInterface');
|
||||
}
|
||||
}
|
||||
|
||||
class DummyUserClass
|
||||
{
|
||||
public function __toString(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\Security\Core\Tests\Authentication\Authenticator;
|
||||
namespace Symfony\Component\Security\Http\Tests\Authenticator;
|
||||
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@ -14,12 +14,10 @@ use Symfony\Component\Security\Http\Authenticator\HttpBasicAuthenticator;
|
||||
|
||||
class HttpBasicAuthenticatorTest extends TestCase
|
||||
{
|
||||
/** @var UserProviderInterface|MockObject */
|
||||
private $userProvider;
|
||||
/** @var EncoderFactoryInterface|MockObject */
|
||||
private $encoderFactory;
|
||||
/** @var PasswordEncoderInterface|MockObject */
|
||||
private $encoder;
|
||||
private $authenticator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
@ -30,17 +28,18 @@ class HttpBasicAuthenticatorTest extends TestCase
|
||||
->expects($this->any())
|
||||
->method('getEncoder')
|
||||
->willReturn($this->encoder);
|
||||
|
||||
$this->authenticator = new HttpBasicAuthenticator('test', $this->userProvider);
|
||||
}
|
||||
|
||||
public function testValidUsernameAndPasswordServerParameters()
|
||||
public function testExtractCredentialsAndUserFromRequest()
|
||||
{
|
||||
$request = new Request([], [], [], [], [], [
|
||||
'PHP_AUTH_USER' => 'TheUsername',
|
||||
'PHP_AUTH_PW' => 'ThePassword',
|
||||
]);
|
||||
|
||||
$authenticator = new HttpBasicAuthenticator('test', $this->userProvider, $this->encoderFactory);
|
||||
$credentials = $authenticator->getCredentials($request);
|
||||
$credentials = $this->authenticator->getCredentials($request);
|
||||
$this->assertEquals([
|
||||
'username' => 'TheUsername',
|
||||
'password' => 'ThePassword',
|
||||
@ -55,53 +54,20 @@ class HttpBasicAuthenticatorTest extends TestCase
|
||||
->with('TheUsername')
|
||||
->willReturn($mockedUser);
|
||||
|
||||
$user = $authenticator->getUser($credentials, $this->userProvider);
|
||||
$user = $this->authenticator->getUser($credentials);
|
||||
$this->assertSame($mockedUser, $user);
|
||||
|
||||
$this->encoder
|
||||
->expects($this->any())
|
||||
->method('isPasswordValid')
|
||||
->with('ThePassword', 'ThePassword', null)
|
||||
->willReturn(true);
|
||||
|
||||
$checkCredentials = $authenticator->checkCredentials($credentials, $user);
|
||||
$this->assertTrue($checkCredentials);
|
||||
$this->assertEquals('ThePassword', $this->authenticator->getPassword($credentials));
|
||||
}
|
||||
|
||||
/** @dataProvider provideInvalidPasswords */
|
||||
public function testInvalidPassword($presentedPassword, $exceptionMessage)
|
||||
{
|
||||
$authenticator = new HttpBasicAuthenticator('test', $this->userProvider, $this->encoderFactory);
|
||||
|
||||
$this->encoder
|
||||
->expects($this->any())
|
||||
->method('isPasswordValid')
|
||||
->willReturn(false);
|
||||
|
||||
$this->expectException(BadCredentialsException::class);
|
||||
$this->expectExceptionMessage($exceptionMessage);
|
||||
|
||||
$authenticator->checkCredentials([
|
||||
'username' => 'TheUsername',
|
||||
'password' => $presentedPassword,
|
||||
], $this->getMockBuilder(UserInterface::class)->getMock());
|
||||
}
|
||||
|
||||
public function provideInvalidPasswords()
|
||||
{
|
||||
return [
|
||||
['InvalidPassword', 'The presented password is invalid.'],
|
||||
['', 'The presented password cannot be empty.'],
|
||||
];
|
||||
}
|
||||
|
||||
/** @dataProvider provideMissingHttpBasicServerParameters */
|
||||
/**
|
||||
* @dataProvider provideMissingHttpBasicServerParameters
|
||||
*/
|
||||
public function testHttpBasicServerParametersMissing(array $serverParameters)
|
||||
{
|
||||
$request = new Request([], [], [], [], [], $serverParameters);
|
||||
|
||||
$authenticator = new HttpBasicAuthenticator('test', $this->userProvider, $this->encoderFactory);
|
||||
$this->assertFalse($authenticator->supports($request));
|
||||
$this->assertFalse($this->authenticator->supports($request));
|
||||
}
|
||||
|
||||
public function provideMissingHttpBasicServerParameters()
|
@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Tests\Authenticator;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Cookie;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
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\Http\Authenticator\RememberMeAuthenticator;
|
||||
use Symfony\Component\Security\Http\RememberMe\AbstractRememberMeServices;
|
||||
|
||||
class RememberMeAuthenticatorTest extends TestCase
|
||||
{
|
||||
private $rememberMeServices;
|
||||
private $tokenStorage;
|
||||
private $authenticator;
|
||||
private $request;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->rememberMeServices = $this->createMock(AbstractRememberMeServices::class);
|
||||
$this->tokenStorage = $this->createMock(TokenStorage::class);
|
||||
$this->authenticator = new RememberMeAuthenticator($this->rememberMeServices, 's3cr3t', $this->tokenStorage, [
|
||||
'name' => '_remember_me_cookie',
|
||||
]);
|
||||
$this->request = new Request();
|
||||
$this->request->cookies->set('_remember_me_cookie', $val = $this->generateCookieValue());
|
||||
$this->request->attributes->set(AbstractRememberMeServices::COOKIE_ATTR_NAME, new Cookie('_remember_me_cookie', $val));
|
||||
}
|
||||
|
||||
public function testSupportsTokenStorageWithToken()
|
||||
{
|
||||
$this->tokenStorage->expects($this->any())->method('getToken')->willReturn(TokenInterface::class);
|
||||
|
||||
$this->assertFalse($this->authenticator->supports($this->request));
|
||||
}
|
||||
|
||||
public function testSupportsRequestWithoutAttribute()
|
||||
{
|
||||
$this->request->attributes->remove(AbstractRememberMeServices::COOKIE_ATTR_NAME);
|
||||
|
||||
$this->assertNull($this->authenticator->supports($this->request));
|
||||
}
|
||||
|
||||
public function testSupportsRequestWithoutCookie()
|
||||
{
|
||||
$this->request->cookies->remove('_remember_me_cookie');
|
||||
|
||||
$this->assertFalse($this->authenticator->supports($this->request));
|
||||
}
|
||||
|
||||
public function testSupports()
|
||||
{
|
||||
$this->assertNull($this->authenticator->supports($this->request));
|
||||
}
|
||||
|
||||
public function testAuthenticate()
|
||||
{
|
||||
$credentials = $this->authenticator->getCredentials($this->request);
|
||||
$this->assertEquals(['part1', 'part2'], $credentials['cookie_parts']);
|
||||
$this->assertSame($this->request, $credentials['request']);
|
||||
|
||||
$user = $this->createMock(UserInterface::class);
|
||||
$this->rememberMeServices->expects($this->any())
|
||||
->method('performLogin')
|
||||
->with($credentials['cookie_parts'], $credentials['request'])
|
||||
->willReturn($user);
|
||||
|
||||
$this->assertSame($user, $this->authenticator->getUser($credentials));
|
||||
}
|
||||
|
||||
public function testCredentialsAlwaysValid()
|
||||
{
|
||||
$this->assertTrue($this->authenticator->checkCredentials([], $this->createMock(UserInterface::class)));
|
||||
}
|
||||
|
||||
private function generateCookieValue()
|
||||
{
|
||||
return base64_encode(implode(AbstractRememberMeServices::COOKIE_DELIMITER, ['part1', 'part2']));
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Tests\EventListener;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
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\AuthenticatorInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\CsrfProtectedAuthenticatorInterface;
|
||||
use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent;
|
||||
use Symfony\Component\Security\Http\EventListener\CsrfProtectionListener;
|
||||
|
||||
class CsrfProtectionListenerTest extends TestCase
|
||||
{
|
||||
private $csrfTokenManager;
|
||||
private $listener;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->csrfTokenManager = $this->createMock(CsrfTokenManagerInterface::class);
|
||||
$this->listener = new CsrfProtectionListener($this->csrfTokenManager);
|
||||
}
|
||||
|
||||
public function testNonCsrfProtectedAuthenticator()
|
||||
{
|
||||
$this->csrfTokenManager->expects($this->never())->method('isTokenValid');
|
||||
|
||||
$event = $this->createEvent($this->createAuthenticator(false));
|
||||
$this->listener->verifyCredentials($event);
|
||||
}
|
||||
|
||||
public function testValidCsrfToken()
|
||||
{
|
||||
$this->csrfTokenManager->expects($this->any())
|
||||
->method('isTokenValid')
|
||||
->with(new CsrfToken('authenticator_token_id', 'abc123'))
|
||||
->willReturn(true);
|
||||
|
||||
$event = $this->createEvent($this->createAuthenticator(true), ['_csrf' => 'abc123']);
|
||||
$this->listener->verifyCredentials($event);
|
||||
|
||||
$this->expectNotToPerformAssertions();
|
||||
}
|
||||
|
||||
public function testInvalidCsrfToken()
|
||||
{
|
||||
$this->expectException(InvalidCsrfTokenException::class);
|
||||
$this->expectExceptionMessage('Invalid CSRF token.');
|
||||
|
||||
$this->csrfTokenManager->expects($this->any())
|
||||
->method('isTokenValid')
|
||||
->with(new CsrfToken('authenticator_token_id', 'abc123'))
|
||||
->willReturn(false);
|
||||
|
||||
$event = $this->createEvent($this->createAuthenticator(true), ['_csrf' => 'abc123']);
|
||||
$this->listener->verifyCredentials($event);
|
||||
}
|
||||
|
||||
private function createEvent($authenticator, $credentials = null)
|
||||
{
|
||||
return new VerifyAuthenticatorCredentialsEvent($authenticator, $credentials, null);
|
||||
}
|
||||
|
||||
private function createAuthenticator($supportsCsrf)
|
||||
{
|
||||
if (!$supportsCsrf) {
|
||||
return $this->createMock(AuthenticatorInterface::class);
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Tests\EventListener;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
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\EventListener\PasswordMigratingListener;
|
||||
|
||||
class PasswordMigratingListenerTest extends TestCase
|
||||
{
|
||||
private $encoderFactory;
|
||||
private $listener;
|
||||
private $user;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->encoderFactory = $this->createMock(EncoderFactoryInterface::class);
|
||||
$this->listener = new PasswordMigratingListener($this->encoderFactory);
|
||||
$this->user = $this->createMock(UserInterface::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideUnsupportedEvents
|
||||
*/
|
||||
public function testUnsupportedEvents($event)
|
||||
{
|
||||
$this->encoderFactory->expects($this->never())->method('getEncoder');
|
||||
|
||||
$this->listener->onCredentialsVerification($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)];
|
||||
|
||||
// null password
|
||||
yield [$this->createEvent($this->createAuthenticator(null), $this->user)];
|
||||
|
||||
// no user
|
||||
yield [$this->createEvent($this->createAuthenticator('pa$$word'), null)];
|
||||
|
||||
// invalid password
|
||||
yield [$this->createEvent($this->createAuthenticator('pa$$word'), $this->user, false)];
|
||||
}
|
||||
|
||||
public function testUpgrade()
|
||||
{
|
||||
$encoder = $this->createMock(PasswordEncoderInterface::class);
|
||||
$encoder->expects($this->any())->method('needsRehash')->willReturn(true);
|
||||
$encoder->expects($this->any())->method('encodePassword')->with('pa$$word', null)->willReturn('new-encoded-password');
|
||||
|
||||
$this->encoderFactory->expects($this->any())->method('getEncoder')->with($this->user)->willReturn($encoder);
|
||||
|
||||
$this->user->expects($this->any())->method('getPassword')->willReturn('old-encoded-password');
|
||||
|
||||
$authenticator = $this->createAuthenticator('pa$$word');
|
||||
$authenticator->expects($this->once())
|
||||
->method('upgradePassword')
|
||||
->with($this->user, 'new-encoded-password')
|
||||
;
|
||||
|
||||
$event = $this->createEvent($authenticator, $this->user);
|
||||
$this->listener->onCredentialsVerification($event);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AuthenticatorInterface
|
||||
*/
|
||||
private function createAuthenticator($password)
|
||||
{
|
||||
$authenticator = $this->createMock([AuthenticatorInterface::class, PasswordAuthenticatedInterface::class, PasswordUpgraderInterface::class]);
|
||||
$authenticator->expects($this->any())->method('getPassword')->willReturn($password);
|
||||
|
||||
return $authenticator;
|
||||
}
|
||||
|
||||
private function createEvent($authenticator, $user, $credentialsValid = true)
|
||||
{
|
||||
$event = new VerifyAuthenticatorCredentialsEvent($authenticator, [], $user);
|
||||
$event->setCredentialsValid($credentialsValid);
|
||||
|
||||
return $event;
|
||||
}
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Tests\EventListener;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
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\Http\Authenticator\AuthenticatorInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\RememberMeAuthenticatorInterface;
|
||||
use Symfony\Component\Security\Http\Event\LoginFailureEvent;
|
||||
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
|
||||
use Symfony\Component\Security\Http\EventListener\RememberMeListener;
|
||||
use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface;
|
||||
|
||||
class RememberMeListenerTest extends TestCase
|
||||
{
|
||||
private $rememberMeServices;
|
||||
private $listener;
|
||||
private $request;
|
||||
private $response;
|
||||
private $token;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->rememberMeServices = $this->createMock(RememberMeServicesInterface::class);
|
||||
$this->listener = new RememberMeListener($this->rememberMeServices);
|
||||
$this->request = $this->getMockBuilder(Request::class)->disableOriginalConstructor()->getMock();
|
||||
$this->response = $this->createMock(Response::class);
|
||||
$this->token = $this->createMock(TokenInterface::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideUnsupportingAuthenticators
|
||||
*/
|
||||
public function testSuccessfulLoginWithoutSupportingAuthenticator($authenticator)
|
||||
{
|
||||
$this->rememberMeServices->expects($this->never())->method('loginSuccess');
|
||||
|
||||
$event = $this->createLoginSuccessfulEvent('main_firewall', $this->response, $authenticator);
|
||||
$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');
|
||||
|
||||
$event = $this->createLoginSuccessfulEvent('main_firewall', null);
|
||||
$this->listener->onSuccessfulLogin($event);
|
||||
}
|
||||
|
||||
public function testSuccessfulLogin()
|
||||
{
|
||||
$this->rememberMeServices->expects($this->once())->method('loginSuccess')->with($this->request, $this->response, $this->token);
|
||||
|
||||
$event = $this->createLoginSuccessfulEvent('main_firewall', $this->response);
|
||||
$this->listener->onSuccessfulLogin($event);
|
||||
}
|
||||
|
||||
public function testCredentialsInvalid()
|
||||
{
|
||||
$this->rememberMeServices->expects($this->once())->method('loginFail')->with($this->request, $this->isInstanceOf(AuthenticationException::class));
|
||||
|
||||
$event = $this->createLoginFailureEvent('main_firewall');
|
||||
$this->listener->onFailedLogin($event);
|
||||
}
|
||||
|
||||
private function createLoginSuccessfulEvent($providerKey, $response, $authenticator = null)
|
||||
{
|
||||
if (null === $authenticator) {
|
||||
$authenticator = $this->createMock([AuthenticatorInterface::class, RememberMeAuthenticatorInterface::class]);
|
||||
$authenticator->expects($this->any())->method('supportsRememberMe')->willReturn(true);
|
||||
}
|
||||
|
||||
return new LoginSuccessEvent($authenticator, $this->token, $this->request, $response, $providerKey);
|
||||
}
|
||||
|
||||
private function createLoginFailureEvent($providerKey)
|
||||
{
|
||||
return new LoginFailureEvent(new AuthenticationException(), $this->createMock(AuthenticatorInterface::class), $this->request, null, $providerKey);
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Tests\EventListener;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
|
||||
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
|
||||
{
|
||||
private $sessionAuthenticationStrategy;
|
||||
private $listener;
|
||||
private $request;
|
||||
private $token;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->sessionAuthenticationStrategy = $this->createMock(SessionAuthenticationStrategyInterface::class);
|
||||
$this->listener = new SessionStrategyListener($this->sessionAuthenticationStrategy);
|
||||
$this->request = new Request();
|
||||
$this->token = $this->createMock(TokenInterface::class);
|
||||
}
|
||||
|
||||
public function testRequestWithSession()
|
||||
{
|
||||
$this->configurePreviousSession();
|
||||
|
||||
$this->sessionAuthenticationStrategy->expects($this->once())->method('onAuthentication')->with($this->request, $this->token);
|
||||
|
||||
$this->listener->onSuccessfulLogin($this->createEvent('main_firewall'));
|
||||
}
|
||||
|
||||
public function testRequestWithoutPreviousSession()
|
||||
{
|
||||
$this->sessionAuthenticationStrategy->expects($this->never())->method('onAuthentication')->with($this->request, $this->token);
|
||||
|
||||
$this->listener->onSuccessfulLogin($this->createEvent('main_firewall'));
|
||||
}
|
||||
|
||||
public function testStatelessFirewalls()
|
||||
{
|
||||
$this->sessionAuthenticationStrategy->expects($this->never())->method('onAuthentication');
|
||||
|
||||
$listener = new SessionStrategyListener($this->sessionAuthenticationStrategy, ['api_firewall']);
|
||||
$listener->onSuccessfulLogin($this->createEvent('api_firewall'));
|
||||
}
|
||||
|
||||
private function createEvent($providerKey)
|
||||
{
|
||||
return new LoginSuccessEvent($this->createMock(AuthenticatorInterface::class), $this->token, $this->request, null, $providerKey);
|
||||
}
|
||||
|
||||
private function configurePreviousSession()
|
||||
{
|
||||
$session = $this->getMockBuilder('Symfony\Component\HttpFoundation\Session\SessionInterface')->getMock();
|
||||
$session->expects($this->any())
|
||||
->method('getName')
|
||||
->willReturn('test_session_name');
|
||||
$this->request->setSession($session);
|
||||
$this->request->cookies->set('test_session_name', 'session_cookie_val');
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Tests\EventListener;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
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\Event\VerifyAuthenticatorCredentialsEvent;
|
||||
use Symfony\Component\Security\Http\EventListener\UserCheckerListener;
|
||||
|
||||
class UserCheckerListenerTest extends TestCase
|
||||
{
|
||||
private $userChecker;
|
||||
private $listener;
|
||||
private $user;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->userChecker = $this->createMock(UserCheckerInterface::class);
|
||||
$this->listener = new UserCheckerListener($this->userChecker);
|
||||
$this->user = $this->createMock(UserInterface::class);
|
||||
}
|
||||
|
||||
public function testPreAuth()
|
||||
{
|
||||
$this->userChecker->expects($this->once())->method('checkPreAuth')->with($this->user);
|
||||
|
||||
$this->listener->preCredentialsVerification($this->createEvent());
|
||||
}
|
||||
|
||||
public function testPreAuthNoUser()
|
||||
{
|
||||
$this->userChecker->expects($this->never())->method('checkPreAuth');
|
||||
|
||||
$this->listener->preCredentialsVerification($this->createEvent(true, null));
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
public function testPostAuthNoUser()
|
||||
{
|
||||
$this->userChecker->expects($this->never())->method('checkPostAuth');
|
||||
|
||||
$this->listener->postCredentialsVerification($this->createEvent(true, null));
|
||||
}
|
||||
|
||||
private function createEvent($credentialsValid = false, $customUser = false)
|
||||
{
|
||||
$event = new VerifyAuthenticatorCredentialsEvent($this->createMock(AuthenticatorInterface::class), [], false === $customUser ? $this->user : $customUser);
|
||||
if ($credentialsValid) {
|
||||
$event->setCredentialsValid(true);
|
||||
}
|
||||
|
||||
return $event;
|
||||
}
|
||||
}
|
@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Tests\EventListener;
|
||||
|
||||
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\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\Event\VerifyAuthenticatorCredentialsEvent;
|
||||
use Symfony\Component\Security\Http\EventListener\VerifyAuthenticatorCredentialsListener;
|
||||
|
||||
class VerifyAuthenticatorCredentialsListenerTest extends TestCase
|
||||
{
|
||||
private $encoderFactory;
|
||||
private $listener;
|
||||
private $user;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->encoderFactory = $this->createMock(EncoderFactoryInterface::class);
|
||||
$this->listener = new VerifyAuthenticatorCredentialsListener($this->encoderFactory);
|
||||
$this->user = $this->createMock(UserInterface::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider providePasswords
|
||||
*/
|
||||
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());
|
||||
}
|
||||
|
||||
public function providePasswords()
|
||||
{
|
||||
yield ['ThePa$$word', true, true];
|
||||
yield ['Invalid', false, false];
|
||||
}
|
||||
|
||||
public function testEmptyPassword()
|
||||
{
|
||||
$this->expectException(BadCredentialsException::class);
|
||||
$this->expectExceptionMessage('The presented password cannot be empty.');
|
||||
|
||||
$this->encoderFactory->expects($this->never())->method('getEncoder');
|
||||
|
||||
$event = new VerifyAuthenticatorCredentialsEvent($this->createAuthenticator('password', ''), ['password' => ''], $this->user);
|
||||
$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
|
||||
*/
|
||||
public function testCustomAuthenticated($result)
|
||||
{
|
||||
$this->encoderFactory->expects($this->never())->method('getEncoder');
|
||||
|
||||
$event = new VerifyAuthenticatorCredentialsEvent($this->createAuthenticator('custom', $result), [], $this->user);
|
||||
$this->listener->onAuthenticating($event);
|
||||
|
||||
$this->assertEquals($result, $event->areCredentialsValid());
|
||||
}
|
||||
|
||||
public function provideCustomAuthenticatedResults()
|
||||
{
|
||||
yield [true];
|
||||
yield [false];
|
||||
}
|
||||
|
||||
public function testAlreadyAuthenticated()
|
||||
{
|
||||
$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);
|
||||
$this->listener->onAuthenticating($event);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AuthenticatorInterface
|
||||
*/
|
||||
private function createAuthenticator(?string $type = null, $result = null)
|
||||
{
|
||||
$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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user