Skip to content

[Security] Add ability for authenticators to explain why they didn’t support a request #60538

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: 7.4
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions UPGRADE-7.4.md
Original file line number Diff line number Diff line change
@@ -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 `$requestSupport` to `AuthenticatorInterface::supports()`;
it should be used to report the reason a request isn't supported. E.g:

```php
public function supports(Request $request, ?RequestSupport $requestSupport = null): ?bool
{
$requestSupport?->addReason('This authenticator does not support any request.');

return false;
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,9 @@
<div id="authenticator-{{ i }}" class="font-normal">
{% if authenticator.supports is same as(false) %}
<div class="empty">
<p>This authenticator did not support the request.</p>
{% for reason in (authenticator.supportReasons ?? []) is empty ? ['This authenticator did not support the request.'] : authenticator.supportReasons %}
<p>{{ reason }}</p>
{% endfor %}
</div>
{% elseif authenticator.authenticated is null %}
<div class="empty">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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\RequestSupport;

class RegisterEntryPointsPassTest extends TestCase
{
Expand Down Expand Up @@ -71,7 +72,7 @@ public function testProcessResolvesChildDefinitionsClass()

class CustomAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface
{
public function supports(Request $request): ?bool
public function supports(Request $request, ?RequestSupport $requestSupport = null): ?bool
{
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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\RequestSupport;

class SecurityExtensionTest extends TestCase
{
Expand Down Expand Up @@ -959,7 +960,7 @@ protected function getContainer()

class TestAuthenticator implements AuthenticatorInterface
{
public function supports(Request $request): ?bool
public function supports(Request $request, ?RequestSupport $requestSupport = null): ?bool
{
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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\RequestSupport;

class ApiAuthenticator extends AbstractAuthenticator
{
Expand All @@ -32,7 +33,7 @@ public function __construct(bool $selfLoadingUser = false)
$this->selfLoadingUser = $selfLoadingUser;
}

public function supports(Request $request): ?bool
public function supports(Request $request, ?RequestSupport $requestSupport = null): ?bool
{
return $request->headers->has('X-USER-EMAIL');
}
Expand Down
5 changes: 3 additions & 2 deletions src/Symfony/Component/Ldap/Security/LdapAuthenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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\RequestSupport;

/**
* This class decorates internal authenticators to add the LDAP integration.
Expand All @@ -44,9 +45,9 @@ public function __construct(
) {
}

public function supports(Request $request): ?bool
public function supports(Request $request, ?RequestSupport $requestSupport = null): ?bool
{
return $this->authenticator->supports($request);
return $this->authenticator->supports($request, $requestSupport);
}

public function authenticate(Request $request): Passport
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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\RequestSupport;
use Symfony\Contracts\Service\ServiceLocatorTrait;

class CheckLdapCredentialsListenerTest extends TestCase
Expand Down Expand Up @@ -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, ?RequestSupport $requestSupport = null): ?bool
{
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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\RequestSupport;
use Symfony\Component\Security\Http\SecurityRequestAttributes;

/**
Expand All @@ -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 RequestSupport|null $requestSupport
*/
public function supports(Request $request): bool
public function supports(Request $request, /* ?RequestSupport $requestSupport = null */): bool
{
return $request->isMethod('POST') && $this->getLoginUrl($request) === $request->getBaseUrl().$request->getPathInfo();
$requestSupport = 2 <= \func_num_args() ? func_get_arg(1) : null;

if (!$request->isMethod('POST')) {
$requestSupport?->addReason('Request is not a POST.');

return false;
}

if ($this->getLoginUrl($request) !== $request->getBaseUrl().$request->getPathInfo()) {
$requestSupport?->addReason('Request does not match the login URL.');

return false;
}

return true;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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\RequestSupport;

/**
* The base authenticator for authenticators to use pre-authenticated
Expand Down Expand Up @@ -52,20 +53,27 @@ public function __construct(
*/
abstract protected function extractUsername(Request $request): ?string;

public function supports(Request $request): ?bool
/**
* @param RequestSupport|null $requestSupport
*/
public function supports(Request $request, /* ?RequestSupport $requestSupport = null */): ?bool
{
$requestSupport = 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]);
$requestSupport?->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]);
$requestSupport?->addReason('Username found in the request was empty.');

return false;
}
Expand All @@ -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]);
$requestSupport?->addReason('A token already exists for the user in the session.');

return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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\RequestSupport;
use Symfony\Contracts\Translation\TranslatorInterface;

/**
Expand All @@ -46,9 +47,20 @@ public function __construct(
) {
}

public function supports(Request $request): ?bool
/**
* @param RequestSupport|null $requestSupport
*/
public function supports(Request $request, /* ?RequestSupport $requestSupport = null */): ?bool
{
return null === $this->accessTokenExtractor->extractAccessToken($request) ? false : null;
$requestSupport = 2 <= \func_num_args() ? func_get_arg(1) : null;

if (null === $this->accessTokenExtractor->extractAccessToken($request)) {
$requestSupport?->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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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\RequestSupport;

/**
* The interface for all authenticators.
Expand All @@ -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 RequestSupport|null $requestSupport
*/
public function supports(Request $request): ?bool;
public function supports(Request $request, /* ?RequestSupport $requestSupport = null */): ?bool;

/**
* Create a passport for the current request.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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\RequestSupport;
use Symfony\Component\VarDumper\Caster\ClassStub;

/**
Expand All @@ -31,6 +32,7 @@
final class TraceableAuthenticator implements AuthenticatorInterface, InteractiveAuthenticatorInterface, AuthenticationEntryPointInterface
{
private ?bool $supports = false;
private array $supportReasons = [];
private ?Passport $passport = null;
private ?float $duration = null;
private ClassStub|string $stub;
Expand All @@ -45,6 +47,7 @@ public function getInfo(): array
{
return [
'supports' => $this->supports,
'supportReasons' => $this->supportReasons,
'passport' => $this->passport,
'duration' => $this->duration,
'stub' => $this->stub ??= class_exists(ClassStub::class) ? new ClassStub($this->authenticator::class) : $this->authenticator::class,
Expand All @@ -62,9 +65,24 @@ static function (BadgeInterface $badge): array {
];
}

public function supports(Request $request): ?bool
/**
* @param RequestSupport|null $requestSupport
*/
public function supports(Request $request, /* ?RequestSupport $requestSupport = null */): ?bool
{
return $this->supports = $this->authenticator->supports($request);
$requestSupport = 2 <= \func_num_args() ? func_get_arg(1) : new RequestSupport();

$this->supports = $this->authenticator->supports($request, $requestSupport);
$this->supportReasons = $requestSupport->reasons;

if ($this->supports === false) {
$requestSupport->result = false;
} else {
$requestSupport->result = true;
$requestSupport->lazy = null === $this->supports;
}

return $this->supports;
}

public function authenticate(Request $request): Passport
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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\RequestSupport;
use Symfony\Component\Security\Http\SecurityRequestAttributes;

/**
Expand Down Expand Up @@ -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 RequestSupport|null $requestSupport
*/
public function supports(Request $request, /* ?RequestSupport $requestSupport = 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);
$requestSupport = 2 <= \func_num_args() ? func_get_arg(1) : null;

if ($this->options['post_only'] && !$request->isMethod('POST')) {
$requestSupport?->addReason('Request is not a POST while "post_only" is set.');

return false;
}

if (!$this->httpUtils->checkRequestPath($request, $this->options['check_path'])) {
$requestSupport?->addReason('Request does not match the "check_path".');

return false;
}

if ($this->options['form_only'] && 'form' !== $request->getContentTypeFormat()) {
$requestSupport?->addReason('Request is not a form submission while "form_only" is set.');

return false;
}

return true;
}

public function authenticate(Request $request): Passport
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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\RequestSupport;

/**
* @author Wouter de Jong <wouter@wouterj.nl>
Expand All @@ -49,9 +50,20 @@ public function start(Request $request, ?AuthenticationException $authException
return $response;
}

public function supports(Request $request): ?bool
/**
* @param RequestSupport|null $requestSupport
*/
public function supports(Request $request, /* ?RequestSupport $requestSupport = null */): ?bool
{
return $request->headers->has('PHP_AUTH_USER');
$requestSupport = 2 <= \func_num_args() ? func_get_arg(1) : null;

if (!$request->headers->has('PHP_AUTH_USER')) {
$requestSupport?->addReason('Request has no basic authentication header.');

return false;
}

return true;
}

public function authenticate(Request $request): Passport
Expand Down
Loading
Loading