diff --git a/UPGRADE-7.4.md b/UPGRADE-7.4.md new file mode 100644 index 0000000000000..9c248bf3589b0 --- /dev/null +++ b/UPGRADE-7.4.md @@ -0,0 +1,24 @@ +UPGRADE FROM 7.3 to 7.4 +======================= + +Symfony 7.4 is a minor release. According to the Symfony release process, there should be no significant +backward compatibility breaks. Minor backward compatibility breaks are prefixed in this document with +`[BC BREAK]`, make sure your code is compatible with these entries before upgrading. +Read more about this in the [Symfony documentation](https://symfony.com/doc/7.4/setup/upgrade_minor.html). + +If you're upgrading from a version below 7.3, follow the [7.3 upgrade guide](UPGRADE-7.3.md) first. + +Security +-------- + + * Add argument `$requestDecision` to `AuthenticatorInterface::supports()`; + it should be used to report the reason a request isn't supported. E.g: + + ```php + public function supports(Request $request, ?RequestDecision $requestDecision = null): ?bool + { + $requestDecision?->addReason('This authenticator does not support any request.'); + + return false; + } + ``` diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig b/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig index f2706858e45cf..5ffa1a1b82a43 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig +++ b/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig @@ -423,7 +423,9 @@
{% if authenticator.supports is same as(false) %}
-

This authenticator did not support the request.

+ {% for reason in (authenticator.requestDecisionReasons ?? []) is empty ? ['This authenticator did not support the request.'] : authenticator.requestDecisionReasons %} +

{{ reason }}

+ {% endfor %}
{% elseif authenticator.authenticated is null %}
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/RegisterEntryPointsPassTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/RegisterEntryPointsPassTest.php index d2fb348676bc7..51003aae1797b 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/RegisterEntryPointsPassTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/RegisterEntryPointsPassTest.php @@ -28,6 +28,7 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; use Symfony\Component\Security\Http\Firewall\ExceptionListener; +use Symfony\Component\Security\Http\RequestDecision; class RegisterEntryPointsPassTest extends TestCase { @@ -71,7 +72,7 @@ public function testProcessResolvesChildDefinitionsClass() class CustomAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface { - public function supports(Request $request): ?bool + public function supports(Request $request, ?RequestDecision $requestDecision = null): ?bool { return false; } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php index d0f3549ab8f09..2de402e45bdcb 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php @@ -38,6 +38,7 @@ use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; use Symfony\Component\Security\Http\Authenticator\HttpBasicAuthenticator; use Symfony\Component\Security\Http\Authenticator\Passport\Passport; +use Symfony\Component\Security\Http\RequestDecision; class SecurityExtensionTest extends TestCase { @@ -959,7 +960,7 @@ protected function getContainer() class TestAuthenticator implements AuthenticatorInterface { - public function supports(Request $request): ?bool + public function supports(Request $request, ?RequestDecision $requestDecision = null): ?bool { } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/ApiAuthenticator.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/ApiAuthenticator.php index 359e61a2b36e4..0690e59abf536 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/ApiAuthenticator.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/ApiAuthenticator.php @@ -22,6 +22,7 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; +use Symfony\Component\Security\Http\RequestDecision; class ApiAuthenticator extends AbstractAuthenticator { @@ -32,7 +33,7 @@ public function __construct(bool $selfLoadingUser = false) $this->selfLoadingUser = $selfLoadingUser; } - public function supports(Request $request): ?bool + public function supports(Request $request, ?RequestDecision $requestDecision = null): ?bool { return $request->headers->has('X-USER-EMAIL'); } diff --git a/src/Symfony/Component/Ldap/Security/LdapAuthenticator.php b/src/Symfony/Component/Ldap/Security/LdapAuthenticator.php index bb98d210af557..e55999e5c7bc6 100644 --- a/src/Symfony/Component/Ldap/Security/LdapAuthenticator.php +++ b/src/Symfony/Component/Ldap/Security/LdapAuthenticator.php @@ -20,6 +20,7 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; use Symfony\Component\Security\Http\EntryPoint\Exception\NotAnEntryPointException; +use Symfony\Component\Security\Http\RequestDecision; /** * This class decorates internal authenticators to add the LDAP integration. @@ -44,9 +45,9 @@ public function __construct( ) { } - public function supports(Request $request): ?bool + public function supports(Request $request, ?RequestDecision $requestDecision = null): ?bool { - return $this->authenticator->supports($request); + return $this->authenticator->supports($request, $requestDecision); } public function authenticate(Request $request): Passport diff --git a/src/Symfony/Component/Ldap/Tests/Security/CheckLdapCredentialsListenerTest.php b/src/Symfony/Component/Ldap/Tests/Security/CheckLdapCredentialsListenerTest.php index 6e69ffe599e75..d4e89697e1390 100644 --- a/src/Symfony/Component/Ldap/Tests/Security/CheckLdapCredentialsListenerTest.php +++ b/src/Symfony/Component/Ldap/Tests/Security/CheckLdapCredentialsListenerTest.php @@ -33,6 +33,7 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; use Symfony\Component\Security\Http\Event\CheckPassportEvent; +use Symfony\Component\Security\Http\RequestDecision; use Symfony\Contracts\Service\ServiceLocatorTrait; class CheckLdapCredentialsListenerTest extends TestCase @@ -206,7 +207,7 @@ private function createListener() if (interface_exists(AuthenticatorInterface::class)) { class TestAuthenticator implements AuthenticatorInterface { - public function supports(Request $request): ?bool + public function supports(Request $request, ?RequestDecision $requestDecision = null): ?bool { } diff --git a/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php index 21835bd32166c..62e679cdd1745 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php @@ -16,6 +16,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; +use Symfony\Component\Security\Http\RequestDecision; use Symfony\Component\Security\Http\SecurityRequestAttributes; /** @@ -36,10 +37,26 @@ abstract protected function getLoginUrl(Request $request): string; * * This default implementation handles all POST requests to the * login path (@see getLoginUrl()). + * + * @param RequestDecision|null $requestDecision */ - public function supports(Request $request): bool + public function supports(Request $request, /* ?RequestDecision $requestDecision = null */): bool { - return $request->isMethod('POST') && $this->getLoginUrl($request) === $request->getBaseUrl().$request->getPathInfo(); + $requestDecision = 2 <= \func_num_args() ? func_get_arg(1) : null; + + if (!$request->isMethod('POST')) { + $requestDecision?->addReason('Request is not a POST.'); + + return false; + } + + if ($this->getLoginUrl($request) !== $request->getBaseUrl().$request->getPathInfo()) { + $requestDecision?->addReason('Request does not match the login URL.'); + + return false; + } + + return true; } /** diff --git a/src/Symfony/Component/Security/Http/Authenticator/AbstractPreAuthenticatedAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AbstractPreAuthenticatedAuthenticator.php index 5017e99162717..2b96e6aa35870 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AbstractPreAuthenticatedAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AbstractPreAuthenticatedAuthenticator.php @@ -24,6 +24,7 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; +use Symfony\Component\Security\Http\RequestDecision; /** * The base authenticator for authenticators to use pre-authenticated @@ -52,20 +53,27 @@ public function __construct( */ abstract protected function extractUsername(Request $request): ?string; - public function supports(Request $request): ?bool + /** + * @param RequestDecision|null $requestDecision + */ + public function supports(Request $request, /* ?RequestDecision $requestDecision = null */): ?bool { + $requestDecision = 2 <= \func_num_args() ? func_get_arg(1) : null; + try { $username = $this->extractUsername($request); } catch (BadCredentialsException $e) { $this->clearToken($e); $this->logger?->debug('Skipping pre-authenticated authenticator as a BadCredentialsException is thrown.', ['exception' => $e, 'authenticator' => static::class]); + $requestDecision?->addReason('Could not find the username in the request.'); return false; } if (null === $username) { $this->logger?->debug('Skipping pre-authenticated authenticator no username could be extracted.', ['authenticator' => static::class]); + $requestDecision?->addReason('Username found in the request was empty.'); return false; } @@ -75,6 +83,7 @@ public function supports(Request $request): ?bool if ($token instanceof PreAuthenticatedToken && $this->firewallName === $token->getFirewallName() && $token->getUserIdentifier() === $username) { $this->logger?->debug('Skipping pre-authenticated authenticator as the user already has an existing session.', ['authenticator' => static::class]); + $requestDecision?->addReason('A token already exists for the user in the session.'); return false; } diff --git a/src/Symfony/Component/Security/Http/Authenticator/AccessTokenAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AccessTokenAuthenticator.php index 75d69ed6437e7..e2a1e33d46130 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AccessTokenAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AccessTokenAuthenticator.php @@ -24,6 +24,7 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; use Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken; +use Symfony\Component\Security\Http\RequestDecision; use Symfony\Contracts\Translation\TranslatorInterface; /** @@ -46,9 +47,20 @@ public function __construct( ) { } - public function supports(Request $request): ?bool + /** + * @param RequestDecision|null $requestDecision + */ + public function supports(Request $request, /* ?RequestDecision $requestDecision = null */): ?bool { - return null === $this->accessTokenExtractor->extractAccessToken($request) ? false : null; + $requestDecision = 2 <= \func_num_args() ? func_get_arg(1) : null; + + if (null === $this->accessTokenExtractor->extractAccessToken($request)) { + $requestDecision?->addReason(sprintf('No token was found in the request by the %s.', $this->accessTokenExtractor::class)); + + return false; + } + + return null; } public function authenticate(Request $request): Passport diff --git a/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php b/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php index 124e0bf94885d..487e04640f395 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php @@ -16,6 +16,7 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Http\Authenticator\Passport\Passport; +use Symfony\Component\Security\Http\RequestDecision; /** * The interface for all authenticators. @@ -32,8 +33,10 @@ interface AuthenticatorInterface * If this returns true, authenticate() will be called. If false, the authenticator will be skipped. * * Returning null means authenticate() can be called lazily when accessing the token storage. + * + * @param RequestDecision|null $requestDecision */ - public function supports(Request $request): ?bool; + public function supports(Request $request, /* ?RequestDecision $requestDecision = null */): ?bool; /** * Create a passport for the current request. diff --git a/src/Symfony/Component/Security/Http/Authenticator/Debug/TraceableAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/Debug/TraceableAuthenticator.php index a98c2bc6564f8..bd32cb16ce202 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/Debug/TraceableAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/Debug/TraceableAuthenticator.php @@ -21,6 +21,7 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; use Symfony\Component\Security\Http\EntryPoint\Exception\NotAnEntryPointException; +use Symfony\Component\Security\Http\RequestDecision; use Symfony\Component\VarDumper\Caster\ClassStub; /** @@ -31,6 +32,7 @@ final class TraceableAuthenticator implements AuthenticatorInterface, InteractiveAuthenticatorInterface, AuthenticationEntryPointInterface { private ?bool $supports = false; + private array $requestDecisionReasons = []; private ?Passport $passport = null; private ?float $duration = null; private ClassStub|string $stub; @@ -45,6 +47,7 @@ public function getInfo(): array { return [ 'supports' => $this->supports, + 'requestDecisionReasons' => $this->requestDecisionReasons, 'passport' => $this->passport, 'duration' => $this->duration, 'stub' => $this->stub ??= class_exists(ClassStub::class) ? new ClassStub($this->authenticator::class) : $this->authenticator::class, @@ -62,9 +65,24 @@ static function (BadgeInterface $badge): array { ]; } - public function supports(Request $request): ?bool + /** + * @param RequestDecision|null $requestDecision + */ + public function supports(Request $request, /* ?RequestDecision $requestDecision = null */): ?bool { - return $this->supports = $this->authenticator->supports($request); + $requestDecision = 2 <= \func_num_args() ? func_get_arg(1) : new RequestDecision(); + + $this->supports = $this->authenticator->supports($request, $requestDecision); + $this->requestDecisionReasons = $requestDecision->reasons; + + if ($this->supports === false) { + $requestDecision->isSupported = false; + } else { + $requestDecision->isSupported = true; + $requestDecision->isLazy = null === $this->supports; + } + + return $this->supports; } public function authenticate(Request $request): Passport diff --git a/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php index d8da062e5fafc..c05f466c3a464 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php @@ -31,6 +31,7 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\HttpUtils; use Symfony\Component\Security\Http\ParameterBagUtils; +use Symfony\Component\Security\Http\RequestDecision; use Symfony\Component\Security\Http\SecurityRequestAttributes; /** @@ -68,11 +69,32 @@ protected function getLoginUrl(Request $request): string return $this->httpUtils->generateUri($request, $this->options['login_path']); } - public function supports(Request $request): bool + /** + * @param RequestDecision|null $requestDecision + */ + public function supports(Request $request, /* ?RequestDecision $requestDecision = null */): bool { - return ($this->options['post_only'] ? $request->isMethod('POST') : true) - && $this->httpUtils->checkRequestPath($request, $this->options['check_path']) - && ($this->options['form_only'] ? 'form' === $request->getContentTypeFormat() : true); + $requestDecision = 2 <= \func_num_args() ? func_get_arg(1) : null; + + if ($this->options['post_only'] && !$request->isMethod('POST')) { + $requestDecision?->addReason('Request is not a POST while "post_only" is set.'); + + return false; + } + + if (!$this->httpUtils->checkRequestPath($request, $this->options['check_path'])) { + $requestDecision?->addReason('Request does not match the "check_path".'); + + return false; + } + + if ($this->options['form_only'] && 'form' !== $request->getContentTypeFormat()) { + $requestDecision?->addReason('Request is not a form submission while "form_only" is set.'); + + return false; + } + + return true; } public function authenticate(Request $request): Passport diff --git a/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php index d76c2be9270cc..a3637a8a0b8f5 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php @@ -24,6 +24,7 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; +use Symfony\Component\Security\Http\RequestDecision; /** * @author Wouter de Jong @@ -49,9 +50,20 @@ public function start(Request $request, ?AuthenticationException $authException return $response; } - public function supports(Request $request): ?bool + /** + * @param RequestDecision|null $requestDecision + */ + public function supports(Request $request, /* ?RequestDecision $requestDecision = null */): ?bool { - return $request->headers->has('PHP_AUTH_USER'); + $requestDecision = 2 <= \func_num_args() ? func_get_arg(1) : null; + + if (!$request->headers->has('PHP_AUTH_USER')) { + $requestDecision?->addReason('Request has no basic authentication header.'); + + return false; + } + + return true; } public function authenticate(Request $request): Passport diff --git a/src/Symfony/Component/Security/Http/Authenticator/JsonLoginAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/JsonLoginAuthenticator.php index b2cd7b42f5465..736c7473a5822 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/JsonLoginAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/JsonLoginAuthenticator.php @@ -31,6 +31,7 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\HttpUtils; +use Symfony\Component\Security\Http\RequestDecision; use Symfony\Contracts\Translation\TranslatorInterface; /** @@ -60,16 +61,25 @@ public function __construct( $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); } - public function supports(Request $request): ?bool + /** + * @param RequestDecision|null $requestDecision + */ + public function supports(Request $request, /* ?RequestDecision $requestDecision = null */): ?bool { + $requestDecision = 2 <= \func_num_args() ? func_get_arg(1) : null; + if ( !str_contains($request->getRequestFormat() ?? '', 'json') && !str_contains($request->getContentTypeFormat() ?? '', 'json') ) { + $requestDecision?->addReason('Request format is not JSON.'); + return false; } if (isset($this->options['check_path']) && !$this->httpUtils->checkRequestPath($request, $this->options['check_path'])) { + $requestDecision?->addReason('Request does not match the "check_path".'); + return false; } diff --git a/src/Symfony/Component/Security/Http/Authenticator/LoginLinkAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/LoginLinkAuthenticator.php index 1547b6e8464f9..67fc5cc1f0450 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/LoginLinkAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/LoginLinkAuthenticator.php @@ -25,6 +25,7 @@ use Symfony\Component\Security\Http\LoginLink\Exception\InvalidLoginLinkAuthenticationException; use Symfony\Component\Security\Http\LoginLink\Exception\InvalidLoginLinkExceptionInterface; use Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface; +use Symfony\Component\Security\Http\RequestDecision; /** * @author Ryan Weaver @@ -43,10 +44,26 @@ public function __construct( $this->options = $options + ['check_post_only' => false]; } - public function supports(Request $request): ?bool + /** + * @param RequestDecision|null $requestDecision + */ + public function supports(Request $request, /* ?RequestDecision $requestDecision = null */): ?bool { - return ($this->options['check_post_only'] ? $request->isMethod('POST') : true) - && $this->httpUtils->checkRequestPath($request, $this->options['check_route']); + $requestDecision = 2 <= \func_num_args() ? func_get_arg(1) : null; + + if ($this->options['check_post_only'] && !$request->isMethod('POST')) { + $requestDecision?->addReason('Request is not a POST while "check_post_only" is set.'); + + return false; + } + + if (!$this->httpUtils->checkRequestPath($request, $this->options['check_route'])) { + $requestDecision?->addReason('Request does not match the "check_route".'); + + return false; + } + + return true; } public function authenticate(Request $request): Passport diff --git a/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php index c695be084861b..dd640c82e4cea 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php @@ -27,6 +27,7 @@ use Symfony\Component\Security\Http\RememberMe\RememberMeDetails; use Symfony\Component\Security\Http\RememberMe\RememberMeHandlerInterface; use Symfony\Component\Security\Http\RememberMe\ResponseListener; +use Symfony\Component\Security\Http\RequestDecision; /** * The RememberMe *Authenticator* performs remember me authentication. @@ -73,18 +74,35 @@ public function __construct( $this->logger = $logger; } - public function supports(Request $request): ?bool + /** + * @param RequestDecision|null $requestDecision + */ + public function supports(Request $request, /* ?RequestDecision $requestDecision = null */): ?bool { + $requestDecision = 2 <= \func_num_args() ? func_get_arg(1) : null; + // do not overwrite already stored tokens (i.e. from the session) if (null !== $this->tokenStorage->getToken()) { + $requestDecision?->addReason('A token already exists in the session.'); + return false; } if (($cookie = $request->attributes->get(ResponseListener::COOKIE_ATTR_NAME)) && null === $cookie->getValue()) { + $requestDecision?->addReason('Cookie is cleared.'); + return false; } - if (!$request->cookies->has($this->cookieName) || !\is_scalar($request->cookies->all()[$this->cookieName] ?: null)) { + if (!$request->cookies->has($this->cookieName)) { + $requestDecision?->addReason(sprintf('Request does not have a "%s" cookie.', $this->cookieName)); + + return false; + } + + if (!\is_scalar($request->cookies->all()[$this->cookieName] ?: null)) { + $requestDecision?->addReason(sprintf('Request does not have a "%s" cookie.', $this->cookieName)); + return false; } diff --git a/src/Symfony/Component/Security/Http/CHANGELOG.md b/src/Symfony/Component/Security/Http/CHANGELOG.md index 275180ff87b3b..35dfcb5292566 100644 --- a/src/Symfony/Component/Security/Http/CHANGELOG.md +++ b/src/Symfony/Component/Security/Http/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.4 +--- + + * Add ability for authenticators to explain why they didn't support a request + 7.3 --- diff --git a/src/Symfony/Component/Security/Http/RequestDecision.php b/src/Symfony/Component/Security/Http/RequestDecision.php new file mode 100644 index 0000000000000..bae22e28fbe9b --- /dev/null +++ b/src/Symfony/Component/Security/Http/RequestDecision.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; + +class RequestDecision +{ + public bool $isSupported; + public ?bool $isLazy = null; + + /** @var list */ + public array $reasons = []; + + public function addReason(string $reason): void + { + $this->reasons[] = $reason; + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/AbstractAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/AbstractAuthenticatorTest.php index 77ca011f66ce0..d15bfe7a8e485 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/AbstractAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/AbstractAuthenticatorTest.php @@ -22,6 +22,7 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; use Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken; +use Symfony\Component\Security\Http\RequestDecision; class AbstractAuthenticatorTest extends TestCase { @@ -42,7 +43,7 @@ public function createToken(Passport $passport, string $firewallName): TokenInte return parent::createToken($passport, $firewallName); } - public function supports(Request $request): ?bool + public function supports(Request $request, ?RequestDecision $requestDecision = null): ?bool { return null; } diff --git a/src/Symfony/Component/Security/Http/Tests/Fixtures/DummyAuthenticator.php b/src/Symfony/Component/Security/Http/Tests/Fixtures/DummyAuthenticator.php index 6e9b6174f1dca..74ea133dcea95 100644 --- a/src/Symfony/Component/Security/Http/Tests/Fixtures/DummyAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Tests/Fixtures/DummyAuthenticator.php @@ -18,13 +18,14 @@ use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; +use Symfony\Component\Security\Http\RequestDecision; /** * @author Alexandre Daubois */ class DummyAuthenticator implements AuthenticatorInterface { - public function supports(Request $request): ?bool + public function supports(Request $request, ?RequestDecision $requestDecision = null): ?bool { return null; } diff --git a/src/Symfony/Component/Security/Http/Tests/Fixtures/DummySupportsAuthenticator.php b/src/Symfony/Component/Security/Http/Tests/Fixtures/DummySupportsAuthenticator.php index 8e7d394a7499a..dae12f10378f0 100644 --- a/src/Symfony/Component/Security/Http/Tests/Fixtures/DummySupportsAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Tests/Fixtures/DummySupportsAuthenticator.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Security\Http\Tests\Fixtures; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Http\RequestDecision; class DummySupportsAuthenticator extends DummyAuthenticator { @@ -22,7 +23,7 @@ public function __construct(?bool $supports) $this->supports = $supports; } - public function supports(Request $request): ?bool + public function supports(Request $request, ?RequestDecision $requestDecision = null): ?bool { return $this->supports; }