diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardAuthenticationFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardAuthenticationFactory.php index 467f3adbd324..a18dfefa3d59 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardAuthenticationFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardAuthenticationFactory.php @@ -15,14 +15,16 @@ use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Security\Guard\Authenticator\GuardBridgeAuthenticator; /** * Configures the "guard" authentication provider key under a firewall. * * @author Ryan Weaver */ -class GuardAuthenticationFactory implements SecurityFactoryInterface +class GuardAuthenticationFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface, EntryPointFactoryInterface { public function getPosition() { @@ -92,6 +94,28 @@ public function create(ContainerBuilder $container, string $id, array $config, s return [$providerId, $listenerId, $entryPointId]; } + public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId) + { + $userProvider = new Reference($userProviderId); + $authenticatorIds = []; + + $guardAuthenticatorIds = $config['authenticators']; + foreach ($guardAuthenticatorIds as $i => $guardAuthenticatorId) { + $container->setDefinition($authenticatorIds[] = 'security.authenticator.guard.'.$firewallName.'.'.$i, new Definition(GuardBridgeAuthenticator::class)) + ->setArguments([ + new Reference($guardAuthenticatorId), + $userProvider, + ]); + } + + return $authenticatorIds; + } + + public function createEntryPoint(ContainerBuilder $container, string $id, array $config, ?string $defaultEntryPointId): string + { + return $this->determineEntryPoint($defaultEntryPointId, $config); + } + private function determineEntryPoint(?string $defaultEntryPointId, array $config): string { if ($defaultEntryPointId) { diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/GuardAuthenticationFactoryTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/GuardAuthenticationFactoryTest.php index fd812c13ae04..291fb1200e4a 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/GuardAuthenticationFactoryTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/GuardAuthenticationFactoryTest.php @@ -17,6 +17,7 @@ use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Security\Guard\Authenticator\GuardBridgeAuthenticator; class GuardAuthenticationFactoryTest extends TestCase { @@ -163,6 +164,29 @@ public function testCreateWithEntryPoint() $this->assertEquals('authenticatorABC', $entryPointId); } + public function testAuthenticatorSystemCreate() + { + $container = new ContainerBuilder(); + $firewallName = 'my_firewall'; + $userProviderId = 'my_user_provider'; + $config = [ + 'authenticators' => ['authenticator123'], + 'entry_point' => null, + ]; + $factory = new GuardAuthenticationFactory(); + + $authenticators = $factory->createAuthenticator($container, $firewallName, $config, $userProviderId); + $this->assertEquals('security.authenticator.guard.my_firewall.0', $authenticators[0]); + + $entryPointId = $factory->createEntryPoint($container, $firewallName, $config, null); + $this->assertEquals('authenticator123', $entryPointId); + + $authenticatorDefinition = $container->getDefinition('security.authenticator.guard.my_firewall.0'); + $this->assertEquals(GuardBridgeAuthenticator::class, $authenticatorDefinition->getClass()); + $this->assertEquals('authenticator123', (string) $authenticatorDefinition->getArgument(0)); + $this->assertEquals($userProviderId, (string) $authenticatorDefinition->getArgument(1)); + } + private function executeCreate(array $config, $defaultEntryPointId) { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/Security/Guard/Authenticator/GuardBridgeAuthenticator.php b/src/Symfony/Component/Security/Guard/Authenticator/GuardBridgeAuthenticator.php new file mode 100644 index 000000000000..e07e8746a85f --- /dev/null +++ b/src/Symfony/Component/Security/Guard/Authenticator/GuardBridgeAuthenticator.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Guard\Authenticator; + +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\Exception\UsernameNotFoundException; +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\Guard\AuthenticatorInterface as GuardAuthenticatorInterface; +use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface; +use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface; +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\CustomCredentials; +use Symfony\Component\Security\Http\Authenticator\Passport\Passport; +use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\UserPassportInterface; + +/** + * This authenticator is used to bridge Guard authenticators with + * the Symfony Authenticator system. + * + * @author Wouter de Jong + * + * @internal + */ +class GuardBridgeAuthenticator implements InteractiveAuthenticatorInterface +{ + private $guard; + private $userProvider; + + public function __construct(GuardAuthenticatorInterface $guard, UserProviderInterface $userProvider) + { + $this->guard = $guard; + $this->userProvider = $userProvider; + } + + public function supports(Request $request): ?bool + { + return $this->guard->supports($request); + } + + public function authenticate(Request $request): PassportInterface + { + $credentials = $this->guard->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_debug_type($this->guard))); + } + + // get the user from the GuardAuthenticator + $user = $this->guard->getUser($credentials, $this->userProvider); + + if (null === $user) { + throw new UsernameNotFoundException(sprintf('Null returned from "%s::getUser()".', get_debug_type($this->guard))); + } + + if (!$user instanceof UserInterface) { + throw new \UnexpectedValueException(sprintf('The "%s::getUser()" method must return a UserInterface. You returned "%s".', get_debug_type($this->guard), get_debug_type($user))); + } + + $passport = new Passport($user, new CustomCredentials([$this->guard, 'checkCredentials'], $credentials)); + if ($this->userProvider instanceof PasswordUpgraderInterface && $this->guard instanceof PasswordAuthenticatedInterface && (null !== $password = $this->guard->getPassword($credentials))) { + $passport->addBadge(new PasswordUpgradeBadge($password, $this->userProvider)); + } + + if ($this->guard->supportsRememberMe()) { + $passport->addBadge(new RememberMeBadge()); + } + + return $passport; + } + + public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface + { + if (!$passport instanceof UserPassportInterface) { + throw new \LogicException(sprintf('"%s" does not support non-user passports.', __CLASS__)); + } + + return $this->guard->createAuthenticatedToken($passport->getUser(), $firewallName); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response + { + return $this->guard->onAuthenticationSuccess($request, $token, $firewallName); + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response + { + return $this->guard->onAuthenticationFailure($request, $exception); + } + + public function isInteractive(): bool + { + // the GuardAuthenticationHandler always dispatches the InteractiveLoginEvent + return true; + } +} diff --git a/src/Symfony/Component/Security/Guard/Tests/Authenticator/GuardBridgeAuthenticatorTest.php b/src/Symfony/Component/Security/Guard/Tests/Authenticator/GuardBridgeAuthenticatorTest.php new file mode 100644 index 000000000000..f6f5c5e54452 --- /dev/null +++ b/src/Symfony/Component/Security/Guard/Tests/Authenticator/GuardBridgeAuthenticatorTest.php @@ -0,0 +1,189 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Guard\Tests\Authenticator; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\Security\Core\User\User; +use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Guard\Authenticator\GuardBridgeAuthenticator; +use Symfony\Component\Security\Guard\AuthenticatorInterface; +use Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials; +use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; + +class GuardBridgeAuthenticatorTest extends TestCase +{ + private $guardAuthenticator; + private $userProvider; + private $authenticator; + + protected function setUp(): void + { + if (!interface_exists(\Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface::class)) { + $this->markTestSkipped('Authenticator system not installed.'); + } + + $this->guardAuthenticator = $this->createMock(AuthenticatorInterface::class); + $this->userProvider = $this->createMock(UserProviderInterface::class); + $this->authenticator = new GuardBridgeAuthenticator($this->guardAuthenticator, $this->userProvider); + } + + public function testSupports() + { + $request = new Request(); + + $this->guardAuthenticator->expects($this->once()) + ->method('supports') + ->with($request) + ->willReturn(true); + + $this->assertTrue($this->authenticator->supports($request)); + } + + public function testNoSupport() + { + $request = new Request(); + + $this->guardAuthenticator->expects($this->once()) + ->method('supports') + ->with($request) + ->willReturn(false); + + $this->assertFalse($this->authenticator->supports($request)); + } + + public function testAuthenticate() + { + $request = new Request(); + + $credentials = ['password' => 's3cr3t']; + $this->guardAuthenticator->expects($this->once()) + ->method('getCredentials') + ->with($request) + ->willReturn($credentials); + + $user = new User('test', null, ['ROLE_USER']); + $this->guardAuthenticator->expects($this->once()) + ->method('getUser') + ->with($credentials, $this->userProvider) + ->willReturn($user); + + $passport = $this->authenticator->authenticate($request); + $this->assertTrue($passport->hasBadge(CustomCredentials::class)); + + $this->guardAuthenticator->expects($this->once()) + ->method('checkCredentials') + ->with($credentials, $user) + ->willReturn(true); + + $passport->getBadge(CustomCredentials::class)->executeCustomChecker($user); + } + + public function testAuthenticateNoUser() + { + $this->expectException(UsernameNotFoundException::class); + + $request = new Request(); + + $credentials = ['password' => 's3cr3t']; + $this->guardAuthenticator->expects($this->once()) + ->method('getCredentials') + ->with($request) + ->willReturn($credentials); + + $this->guardAuthenticator->expects($this->once()) + ->method('getUser') + ->with($credentials, $this->userProvider) + ->willReturn(null); + + $this->authenticator->authenticate($request); + } + + /** + * @dataProvider provideRememberMeData + */ + public function testAuthenticateRememberMe(bool $rememberMeSupported) + { + $request = new Request(); + + $credentials = ['password' => 's3cr3t']; + $this->guardAuthenticator->expects($this->once()) + ->method('getCredentials') + ->with($request) + ->willReturn($credentials); + + $user = new User('test', null, ['ROLE_USER']); + $this->guardAuthenticator->expects($this->once()) + ->method('getUser') + ->with($credentials, $this->userProvider) + ->willReturn($user); + + $this->guardAuthenticator->expects($this->once()) + ->method('supportsRememberMe') + ->willReturn($rememberMeSupported); + + $passport = $this->authenticator->authenticate($request); + $this->assertEquals($rememberMeSupported, $passport->hasBadge(RememberMeBadge::class)); + } + + public function provideRememberMeData() + { + yield [true]; + yield [false]; + } + + public function testCreateAuthenticatedToken() + { + $user = new User('test', null, ['ROLE_USER']); + + $token = new PostAuthenticationGuardToken($user, 'main', ['ROLE_USER']); + $this->guardAuthenticator->expects($this->once()) + ->method('createAuthenticatedToken') + ->with($user, 'main') + ->willReturn($token); + + $this->assertSame($token, $this->authenticator->createAuthenticatedToken(new SelfValidatingPassport($user), 'main')); + } + + public function testHandleSuccess() + { + $request = new Request(); + $token = new PostAuthenticationGuardToken(new User('test', null, ['ROLE_USER']), 'main', ['ROLE_USER']); + + $response = new Response(); + $this->guardAuthenticator->expects($this->once()) + ->method('onAuthenticationSuccess') + ->with($request, $token) + ->willReturn($response); + + $this->assertSame($response, $this->authenticator->onAuthenticationSuccess($request, $token, 'main')); + } + + public function testOnFailure() + { + $request = new Request(); + $exception = new AuthenticationException(); + + $response = new Response(); + $this->guardAuthenticator->expects($this->once()) + ->method('onAuthenticationFailure') + ->with($request, $exception) + ->willReturn($response); + + $this->assertSame($response, $this->authenticator->onAuthenticationFailure($request, $exception)); + } +}