diff --git a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php index 0e2a653ddfa1b..82c8cd2e7b7a7 100644 --- a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php +++ b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php @@ -25,6 +25,7 @@ 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\Debug\TraceableAuthenticator; use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; @@ -151,7 +152,7 @@ private function executeAuthenticators(array $authenticators, Request $request): // lazily (after initialization). if (false === $authenticator->supports($request)) { if (null !== $this->logger) { - $this->logger->debug('Skipping the "{authenticator}" authenticator as it did not support the request.', ['authenticator' => \get_class($authenticator)]); + $this->logger->debug('Skipping the "{authenticator}" authenticator as it did not support the request.', ['authenticator' => \get_class($authenticator instanceof TraceableAuthenticator ? $authenticator->getAuthenticator() : $authenticator)]); } continue; @@ -160,7 +161,7 @@ private function executeAuthenticators(array $authenticators, Request $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)]); + $this->logger->debug('The "{authenticator}" authenticator set the response. Any later authenticator will not be called', ['authenticator' => \get_class($authenticator instanceof TraceableAuthenticator ? $authenticator->getAuthenticator() : $authenticator)]); } return $response; @@ -210,7 +211,7 @@ private function executeAuthenticator(AuthenticatorInterface $authenticator, Req $this->eventDispatcher->dispatch(new AuthenticationSuccessEvent($authenticatedToken), AuthenticationEvents::AUTHENTICATION_SUCCESS); if (null !== $this->logger) { - $this->logger->info('Authenticator successful!', ['token' => $authenticatedToken, 'authenticator' => \get_class($authenticator)]); + $this->logger->info('Authenticator successful!', ['token' => $authenticatedToken, 'authenticator' => \get_class($authenticator instanceof TraceableAuthenticator ? $authenticator->getAuthenticator() : $authenticator)]); } } catch (AuthenticationException $e) { // oh no! Authentication failed! @@ -229,7 +230,7 @@ private function executeAuthenticator(AuthenticatorInterface $authenticator, Req } if (null !== $this->logger) { - $this->logger->debug('Authenticator set no success response: request continues.', ['authenticator' => \get_class($authenticator)]); + $this->logger->debug('Authenticator set no success response: request continues.', ['authenticator' => \get_class($authenticator instanceof TraceableAuthenticator ? $authenticator->getAuthenticator() : $authenticator)]); } return null; @@ -262,7 +263,7 @@ private function handleAuthenticationSuccess(TokenInterface $authenticatedToken, private function handleAuthenticationFailure(AuthenticationException $authenticationException, Request $request, AuthenticatorInterface $authenticator, ?PassportInterface $passport): ?Response { if (null !== $this->logger) { - $this->logger->info('Authenticator failed.', ['exception' => $authenticationException, 'authenticator' => \get_class($authenticator)]); + $this->logger->info('Authenticator failed.', ['exception' => $authenticationException, 'authenticator' => \get_class($authenticator instanceof TraceableAuthenticator ? $authenticator->getAuthenticator() : $authenticator)]); } // Avoid leaking error details in case of invalid user (e.g. user not found or invalid account status) @@ -273,7 +274,7 @@ private function handleAuthenticationFailure(AuthenticationException $authentica $response = $authenticator->onAuthenticationFailure($request, $authenticationException); if (null !== $response && null !== $this->logger) { - $this->logger->debug('The "{authenticator}" authenticator set the failure response.', ['authenticator' => \get_class($authenticator)]); + $this->logger->debug('The "{authenticator}" authenticator set the failure response.', ['authenticator' => \get_class($authenticator instanceof TraceableAuthenticator ? $authenticator->getAuthenticator() : $authenticator)]); } $this->eventDispatcher->dispatch($loginFailureEvent = new LoginFailureEvent($authenticationException, $authenticator, $request, $response, $this->firewallName, $passport)); diff --git a/src/Symfony/Component/Security/Http/Authenticator/Debug/TraceableAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/Debug/TraceableAuthenticator.php index d7063e5aa4ed4..4b77d9c49f138 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/Debug/TraceableAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/Debug/TraceableAuthenticator.php @@ -100,6 +100,14 @@ public function isInteractive(): bool return $this->authenticator instanceof InteractiveAuthenticatorInterface && $this->authenticator->isInteractive(); } + /** + * @internal + */ + public function getAuthenticator(): AuthenticatorInterface + { + return $this->authenticator; + } + public function __call($method, $args) { return $this->authenticator->{$method}(...$args); diff --git a/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php b/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php index f00009134eaaa..96dea2730aa24 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php @@ -12,6 +12,8 @@ namespace Symfony\Component\Security\Http\Tests\Authentication; use PHPUnit\Framework\TestCase; +use Psr\Log\AbstractLogger; +use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -22,6 +24,7 @@ use Symfony\Component\Security\Core\Exception\UserNotFoundException; use Symfony\Component\Security\Core\User\InMemoryUser; use Symfony\Component\Security\Http\Authentication\AuthenticatorManager; +use Symfony\Component\Security\Http\Authenticator\Debug\TraceableAuthenticator; use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; @@ -310,6 +313,44 @@ public function testAuthenticateRequestHidesInvalidUserExceptions() $this->assertSame($this->response, $response); } + public function testLogsUseTheDecoratedAuthenticatorWhenItIsTraceable() + { + $authenticator = $this->createMock(TestInteractiveAuthenticator::class); + $authenticator->expects($this->any())->method('isInteractive')->willReturn(true); + $this->request->attributes->set('_security_authenticators', [new TraceableAuthenticator($authenticator)]); + + $authenticator->expects($this->any())->method('authenticate')->willReturn(new SelfValidatingPassport(new UserBadge('wouter', function () { return $this->user; }))); + $authenticator->expects($this->any())->method('createToken')->willReturn($this->token); + + $this->tokenStorage->expects($this->once())->method('setToken')->with($this->token); + + $authenticator->expects($this->any()) + ->method('onAuthenticationSuccess') + ->with($this->anything(), $this->token, 'main') + ->willReturn($this->response); + + $authenticator->expects($this->any()) + ->method('onAuthenticationSuccess') + ->with($this->anything(), $this->token, 'main') + ->willReturn($this->response); + + $logger = new class() extends AbstractLogger { + public $logContexts = []; + + public function log($level, $message, array $context = []): void + { + if ($context['authenticator'] ?? false) { + $this->logContexts[] = $context; + } + } + }; + + $manager = $this->createManager([$authenticator], 'main', true, [], $logger); + $response = $manager->authenticateRequest($this->request); + $this->assertSame($this->response, $response); + $this->assertStringContainsString('Mock_TestInteractiveAuthenticator', $logger->logContexts[0]['authenticator']); + } + private function createAuthenticator($supports = true) { $authenticator = $this->createMock(TestInteractiveAuthenticator::class); @@ -318,9 +359,9 @@ private function createAuthenticator($supports = true) return $authenticator; } - private function createManager($authenticators, $firewallName = 'main', $eraseCredentials = true, array $requiredBadges = []) + private function createManager($authenticators, $firewallName = 'main', $eraseCredentials = true, array $requiredBadges = [], LoggerInterface $logger = null) { - return new AuthenticatorManager($authenticators, $this->tokenStorage, $this->eventDispatcher, $firewallName, null, $eraseCredentials, true, $requiredBadges); + return new AuthenticatorManager($authenticators, $this->tokenStorage, $this->eventDispatcher, $firewallName, $logger, $eraseCredentials, true, $requiredBadges); } }