Skip to content

Commit b0cdff7

Browse files
committed
[Security] Add ability for authenticators to explain why they supported the request or not
1 parent cdcd640 commit b0cdff7

15 files changed

+177
-25
lines changed

src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,9 @@
423423
<div id="authenticator-{{ i }}" class="font-normal">
424424
{% if authenticator.supports is same as(false) %}
425425
<div class="empty">
426-
<p>This authenticator did not support the request.</p>
426+
{% for reason in (authenticator.supportReasons ?? []) is empty ? ['This authenticator did not support the request.'] : authenticator.supportReasons %}
427+
<p>{{ reason }}</p>
428+
{% endfor %}
427429
</div>
428430
{% elseif authenticator.authenticated is null %}
429431
<div class="empty">

src/Symfony/Component/Ldap/Security/LdapAuthenticator.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
2121
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
2222
use Symfony\Component\Security\Http\EntryPoint\Exception\NotAnEntryPointException;
23+
use Symfony\Component\Security\Http\RequestSupport;
2324

2425
/**
2526
* This class decorates internal authenticators to add the LDAP integration.
@@ -44,9 +45,9 @@ public function __construct(
4445
) {
4546
}
4647

47-
public function supports(Request $request): ?bool
48+
public function supports(Request $request, ?RequestSupport $requestSupport = null): ?bool
4849
{
49-
return $this->authenticator->supports($request);
50+
return $this->authenticator->supports($request, $requestSupport);
5051
}
5152

5253
public function authenticate(Request $request): Passport

src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
3838
use Symfony\Component\Security\Http\Event\LoginFailureEvent;
3939
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
40+
use Symfony\Component\Security\Http\RequestSupport;
4041
use Symfony\Component\Security\Http\SecurityEvents;
4142
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
4243

@@ -117,12 +118,18 @@ public function supports(Request $request): ?bool
117118
throw new \InvalidArgumentException(\sprintf('Authenticator "%s" must implement "%s".', get_debug_type($authenticator), AuthenticatorInterface::class));
118119
}
119120

120-
if (false !== $supports = $authenticator->supports($request)) {
121+
$requestSupport = new RequestSupport();
122+
if (false !== $supports = $authenticator->supports($request, $requestSupport)) {
121123
$authenticators[] = $authenticator;
122124
$lazy = $lazy && null === $supports;
125+
126+
$requestSupport->result = true;
127+
$requestSupport->lazy = $lazy;
123128
} else {
124129
$this->logger?->debug('Authenticator does not support the request.', ['firewall_name' => $this->firewallName, 'authenticator' => $authenticator::class]);
125130
$skippedAuthenticators[] = $authenticator;
131+
132+
$requestSupport->result = false;
126133
}
127134
}
128135

@@ -158,8 +165,10 @@ private function executeAuthenticators(array $authenticators, Request $request):
158165
// recheck if the authenticator still supports the listener. supports() is called
159166
// eagerly (before token storage is initialized), whereas authenticate() is called
160167
// lazily (after initialization).
161-
if (false === $authenticator->supports($request)) {
168+
$requestSupport = new RequestSupport();
169+
if (false === $authenticator->supports($request, $requestSupport)) {
162170
$this->logger?->debug('Skipping the "{authenticator}" authenticator as it did not support the request.', ['authenticator' => ($authenticator instanceof TraceableAuthenticator ? $authenticator->getAuthenticator() : $authenticator)::class]);
171+
$requestSupport->result = false;
163172

164173
continue;
165174
}

src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Symfony\Component\HttpFoundation\Response;
1717
use Symfony\Component\Security\Core\Exception\AuthenticationException;
1818
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
19+
use Symfony\Component\Security\Http\RequestSupport;
1920
use Symfony\Component\Security\Http\SecurityRequestAttributes;
2021

2122
/**
@@ -37,9 +38,24 @@ abstract protected function getLoginUrl(Request $request): string;
3738
* This default implementation handles all POST requests to the
3839
* login path (@see getLoginUrl()).
3940
*/
40-
public function supports(Request $request): bool
41+
public function supports(Request $request, /* ?RequestSupport $requestSupport = null */): bool
4142
{
42-
return $request->isMethod('POST') && $this->getLoginUrl($request) === $request->getBaseUrl().$request->getPathInfo();
43+
$requestSupport = 2 <= \func_num_args() ? func_get_arg(1) : new RequestSupport();
44+
$requestSupport ??= new RequestSupport();
45+
46+
if (!$request->isMethod('POST')) {
47+
$requestSupport->addReason('Request is not a POST.');
48+
49+
return false;
50+
}
51+
52+
if ($this->getLoginUrl($request) !== $request->getBaseUrl().$request->getPathInfo()) {
53+
$requestSupport->addReason('Request does not match the login URL.');
54+
55+
return false;
56+
}
57+
58+
return true;
4359
}
4460

4561
/**

src/Symfony/Component/Security/Http/Authenticator/AbstractPreAuthenticatedAuthenticator.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
2525
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
2626
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
27+
use Symfony\Component\Security\Http\RequestSupport;
2728

2829
/**
2930
* The base authenticator for authenticators to use pre-authenticated
@@ -52,20 +53,25 @@ public function __construct(
5253
*/
5354
abstract protected function extractUsername(Request $request): ?string;
5455

55-
public function supports(Request $request): ?bool
56+
public function supports(Request $request, /* ?RequestSupport $requestSupport = null */): ?bool
5657
{
58+
$requestSupport = 2 <= \func_num_args() ? func_get_arg(1) : new RequestSupport();
59+
$requestSupport ??= new RequestSupport();
60+
5761
try {
5862
$username = $this->extractUsername($request);
5963
} catch (BadCredentialsException $e) {
6064
$this->clearToken($e);
6165

6266
$this->logger?->debug('Skipping pre-authenticated authenticator as a BadCredentialsException is thrown.', ['exception' => $e, 'authenticator' => static::class]);
67+
$requestSupport->addReason('Could not find the username in the request.');
6368

6469
return false;
6570
}
6671

6772
if (null === $username) {
6873
$this->logger?->debug('Skipping pre-authenticated authenticator no username could be extracted.', ['authenticator' => static::class]);
74+
$requestSupport->addReason('Username found in the request was empty.');
6975

7076
return false;
7177
}
@@ -75,6 +81,7 @@ public function supports(Request $request): ?bool
7581

7682
if ($token instanceof PreAuthenticatedToken && $this->firewallName === $token->getFirewallName() && $token->getUserIdentifier() === $username) {
7783
$this->logger?->debug('Skipping pre-authenticated authenticator as the user already has an existing session.', ['authenticator' => static::class]);
84+
$requestSupport->addReason('A token already exists for the user in the session.');
7885

7986
return false;
8087
}

src/Symfony/Component/Security/Http/Authenticator/AccessTokenAuthenticator.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
2525
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
2626
use Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken;
27+
use Symfony\Component\Security\Http\RequestSupport;
2728
use Symfony\Contracts\Translation\TranslatorInterface;
2829

2930
/**
@@ -46,9 +47,18 @@ public function __construct(
4647
) {
4748
}
4849

49-
public function supports(Request $request): ?bool
50+
public function supports(Request $request, /* ?RequestSupport $requestSupport = null */): ?bool
5051
{
51-
return null === $this->accessTokenExtractor->extractAccessToken($request) ? false : null;
52+
$requestSupport = 2 <= \func_num_args() ? func_get_arg(1) : new RequestSupport();
53+
$requestSupport ??= new RequestSupport();
54+
55+
if (null === $this->accessTokenExtractor->extractAccessToken($request)) {
56+
$requestSupport->addReason(sprintf('No token was found in the request by the %s.', $this->accessTokenExtractor::class));
57+
58+
return false;
59+
}
60+
61+
return null;
5262
}
5363

5464
public function authenticate(Request $request): Passport

src/Symfony/Component/Security/Http/Authenticator/AuthenticatorInterface.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
1717
use Symfony\Component\Security\Core\Exception\AuthenticationException;
1818
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
19+
use Symfony\Component\Security\Http\RequestSupport;
1920

2021
/**
2122
* The interface for all authenticators.
@@ -32,8 +33,10 @@ interface AuthenticatorInterface
3233
* If this returns true, authenticate() will be called. If false, the authenticator will be skipped.
3334
*
3435
* Returning null means authenticate() can be called lazily when accessing the token storage.
36+
*
37+
* @param RequestSupport|null $requestSupport
3538
*/
36-
public function supports(Request $request): ?bool;
39+
public function supports(Request $request, /* ?RequestSupport $requestSupport = null */): ?bool;
3740

3841
/**
3942
* Create a passport for the current request.

src/Symfony/Component/Security/Http/Authenticator/Debug/TraceableAuthenticator.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
2222
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
2323
use Symfony\Component\Security\Http\EntryPoint\Exception\NotAnEntryPointException;
24+
use Symfony\Component\Security\Http\RequestSupport;
2425
use Symfony\Component\VarDumper\Caster\ClassStub;
2526

2627
/**
@@ -31,6 +32,7 @@
3132
final class TraceableAuthenticator implements AuthenticatorInterface, InteractiveAuthenticatorInterface, AuthenticationEntryPointInterface
3233
{
3334
private ?bool $supports = false;
35+
private array $supportReasons = [];
3436
private ?Passport $passport = null;
3537
private ?float $duration = null;
3638
private ClassStub|string $stub;
@@ -45,6 +47,7 @@ public function getInfo(): array
4547
{
4648
return [
4749
'supports' => $this->supports,
50+
'supportReasons' => $this->supportReasons,
4851
'passport' => $this->passport,
4952
'duration' => $this->duration,
5053
'stub' => $this->stub ??= class_exists(ClassStub::class) ? new ClassStub($this->authenticator::class) : $this->authenticator::class,
@@ -62,9 +65,14 @@ static function (BadgeInterface $badge): array {
6265
];
6366
}
6467

65-
public function supports(Request $request): ?bool
68+
public function supports(Request $request, /* ?RequestSupport $requestSupport = null */): ?bool
6669
{
67-
return $this->supports = $this->authenticator->supports($request);
70+
$requestSupport = 2 <= \func_num_args() ? func_get_arg(1) : new RequestSupport();
71+
72+
$this->supports = $this->authenticator->supports($request, $requestSupport);
73+
$this->supportReasons = $requestSupport->reasons;
74+
75+
return $this->supports;
6876
}
6977

7078
public function authenticate(Request $request): Passport

src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
3232
use Symfony\Component\Security\Http\HttpUtils;
3333
use Symfony\Component\Security\Http\ParameterBagUtils;
34+
use Symfony\Component\Security\Http\RequestSupport;
3435
use Symfony\Component\Security\Http\SecurityRequestAttributes;
3536

3637
/**
@@ -68,11 +69,30 @@ protected function getLoginUrl(Request $request): string
6869
return $this->httpUtils->generateUri($request, $this->options['login_path']);
6970
}
7071

71-
public function supports(Request $request): bool
72+
public function supports(Request $request, /* ?RequestSupport $requestSupport = null */): bool
7273
{
73-
return ($this->options['post_only'] ? $request->isMethod('POST') : true)
74-
&& $this->httpUtils->checkRequestPath($request, $this->options['check_path'])
75-
&& ($this->options['form_only'] ? 'form' === $request->getContentTypeFormat() : true);
74+
$requestSupport = 2 <= \func_num_args() ? func_get_arg(1) : new RequestSupport();
75+
$requestSupport ??= new RequestSupport();
76+
77+
if ($this->options['post_only'] && !$request->isMethod('POST')) {
78+
$requestSupport->addReason('Request is not a POST while "post_only" is set.');
79+
80+
return false;
81+
}
82+
83+
if (!$this->httpUtils->checkRequestPath($request, $this->options['check_path'])) {
84+
$requestSupport->addReason('Request does not match the "check_path".');
85+
86+
return false;
87+
}
88+
89+
if ($this->options['form_only'] && 'form' !== $request->getContentTypeFormat()) {
90+
$requestSupport->addReason('Request is not a form submission while "form_only" is set.');
91+
92+
return false;
93+
}
94+
95+
return true;
7696
}
7797

7898
public function authenticate(Request $request): Passport

src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
2525
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
2626
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
27+
use Symfony\Component\Security\Http\RequestSupport;
2728

2829
/**
2930
* @author Wouter de Jong <wouter@wouterj.nl>
@@ -49,9 +50,18 @@ public function start(Request $request, ?AuthenticationException $authException
4950
return $response;
5051
}
5152

52-
public function supports(Request $request): ?bool
53+
public function supports(Request $request, /* ?RequestSupport $requestSupport = null */): ?bool
5354
{
54-
return $request->headers->has('PHP_AUTH_USER');
55+
$requestSupport = 2 <= \func_num_args() ? func_get_arg(1) : new RequestSupport();
56+
$requestSupport ??= new RequestSupport();
57+
58+
if (!$request->headers->has('PHP_AUTH_USER')) {
59+
$requestSupport->addReason('Request has no basic authentication header.');
60+
61+
return false;
62+
}
63+
64+
return true;
5565
}
5666

5767
public function authenticate(Request $request): Passport

src/Symfony/Component/Security/Http/Authenticator/JsonLoginAuthenticator.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
3232
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
3333
use Symfony\Component\Security\Http\HttpUtils;
34+
use Symfony\Component\Security\Http\RequestSupport;
3435
use Symfony\Contracts\Translation\TranslatorInterface;
3536

3637
/**
@@ -60,16 +61,23 @@ public function __construct(
6061
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
6162
}
6263

63-
public function supports(Request $request): ?bool
64+
public function supports(Request $request, /* ?RequestSupport $requestSupport = null */): ?bool
6465
{
66+
$requestSupport = 2 <= \func_num_args() ? func_get_arg(1) : new RequestSupport();
67+
$requestSupport ??= new RequestSupport();
68+
6569
if (
6670
!str_contains($request->getRequestFormat() ?? '', 'json')
6771
&& !str_contains($request->getContentTypeFormat() ?? '', 'json')
6872
) {
73+
$requestSupport->addReason('Request format is not JSON.');
74+
6975
return false;
7076
}
7177

7278
if (isset($this->options['check_path']) && !$this->httpUtils->checkRequestPath($request, $this->options['check_path'])) {
79+
$requestSupport->addReason('Request does not match the "check_path".');
80+
7381
return false;
7482
}
7583

src/Symfony/Component/Security/Http/Authenticator/LoginLinkAuthenticator.php

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use Symfony\Component\Security\Http\LoginLink\Exception\InvalidLoginLinkAuthenticationException;
2626
use Symfony\Component\Security\Http\LoginLink\Exception\InvalidLoginLinkExceptionInterface;
2727
use Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface;
28+
use Symfony\Component\Security\Http\RequestSupport;
2829

2930
/**
3031
* @author Ryan Weaver <ryan@symfonycasts.com>
@@ -43,10 +44,24 @@ public function __construct(
4344
$this->options = $options + ['check_post_only' => false];
4445
}
4546

46-
public function supports(Request $request): ?bool
47+
public function supports(Request $request, /* ?RequestSupport $requestSupport = null */): ?bool
4748
{
48-
return ($this->options['check_post_only'] ? $request->isMethod('POST') : true)
49-
&& $this->httpUtils->checkRequestPath($request, $this->options['check_route']);
49+
$requestSupport = 2 <= \func_num_args() ? func_get_arg(1) : new RequestSupport();
50+
$requestSupport ??= new RequestSupport();
51+
52+
if ($this->options['check_post_only'] && !$request->isMethod('POST')) {
53+
$requestSupport->addReason('Request is not a POST while "check_post_only" is set.');
54+
55+
return false;
56+
}
57+
58+
if (!$this->httpUtils->checkRequestPath($request, $this->options['check_route'])) {
59+
$requestSupport->addReason('Request does not match the "check_route".');
60+
61+
return false;
62+
}
63+
64+
return true;
5065
}
5166

5267
public function authenticate(Request $request): Passport

0 commit comments

Comments
 (0)