The start of something beautiful

This commit is contained in:
2024-09-11 22:48:07 -06:00
parent 45acea47f3
commit f5997ee5ec
5614 changed files with 630696 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
/**
* AccessMap allows configuration of different access control rules for
* specific parts of the website.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class AccessMap implements AccessMapInterface
{
private array $map = [];
/**
* @param array $attributes An array of attributes to pass to the access decision manager (like roles)
* @param string|null $channel The channel to enforce (http, https, or null)
*/
public function add(RequestMatcherInterface $requestMatcher, array $attributes = [], ?string $channel = null): void
{
$this->map[] = [$requestMatcher, $attributes, $channel];
}
public function getPatterns(Request $request): array
{
foreach ($this->map as $elements) {
if (null === $elements[0] || $elements[0]->matches($request)) {
return [$elements[1], $elements[2]];
}
}
return [null, null];
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http;
use Symfony\Component\HttpFoundation\Request;
/**
* AccessMap allows configuration of different access control rules for
* specific parts of the website.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Kris Wallsmith <kris@symfony.com>
*/
interface AccessMapInterface
{
/**
* Returns security attributes and required channel for the supplied request.
*
* @return array{0: array|null, 1: string|null} A tuple of security attributes and the required channel
*/
public function getPatterns(Request $request): array;
}
@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\AccessToken;
use Symfony\Component\HttpFoundation\Request;
/**
* The token extractor retrieves the token from a request.
*
* @author Florent Morselli <florent.morselli@spomky-labs.com>
*/
interface AccessTokenExtractorInterface
{
public function extractAccessToken(Request $request): ?string;
}
@@ -0,0 +1,29 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\AccessToken;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
/**
* The token handler retrieves the user identifier from the token.
* In order to get the user identifier, implementations may need to load and validate the token (e.g. revocation, expiration time, digital signature...).
*
* @author Florent Morselli <florent.morselli@spomky-labs.com>
*/
interface AccessTokenHandlerInterface
{
/**
* @throws AuthenticationException
*/
public function getUserBadgeFrom(#[\SensitiveParameter] string $accessToken): UserBadge;
}
@@ -0,0 +1,85 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\AccessToken\Cas;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
* @see https://apereo.github.io/cas/6.6.x/protocol/CAS-Protocol-V2-Specification.html
*
* @author Nicolas Attard <contact@nicolasattard.fr>
*/
final class Cas2Handler implements AccessTokenHandlerInterface
{
public function __construct(
private readonly RequestStack $requestStack,
private readonly string $validationUrl,
private readonly string $prefix = 'cas',
private ?HttpClientInterface $client = null,
) {
if (null === $client) {
if (!class_exists(HttpClient::class)) {
throw new \LogicException(sprintf('You cannot use "%s" as the HttpClient component is not installed. Try running "composer require symfony/http-client".', __CLASS__));
}
$this->client = HttpClient::create();
}
}
/**
* @throws AuthenticationException
*/
public function getUserBadgeFrom(string $accessToken): UserBadge
{
$response = $this->client->request('GET', $this->getValidationUrl($accessToken));
$xml = new \SimpleXMLElement($response->getContent(), 0, false, $this->prefix, true);
if (isset($xml->authenticationSuccess)) {
return new UserBadge((string) $xml->authenticationSuccess->user);
}
if (isset($xml->authenticationFailure)) {
throw new AuthenticationException('CAS Authentication Failure: '.trim((string) $xml->authenticationFailure));
}
throw new AuthenticationException('Invalid CAS response.');
}
private function getValidationUrl(string $accessToken): string
{
$request = $this->requestStack->getCurrentRequest();
if (null === $request) {
throw new \LogicException('Request should exist so it can be processed for error.');
}
$query = $request->query->all();
if (!isset($query['ticket'])) {
throw new AuthenticationException('No ticket found in request.');
}
unset($query['ticket']);
$queryString = $query ? '?'.http_build_query($query) : '';
return sprintf('%s?ticket=%s&service=%s',
$this->validationUrl,
urlencode($accessToken),
urlencode($request->getSchemeAndHttpHost().$request->getBaseUrl().$request->getPathInfo().$queryString)
);
}
}
@@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\AccessToken;
use Symfony\Component\HttpFoundation\Request;
/**
* The token extractor retrieves the token from a request.
*
* @author Florent Morselli <florent.morselli@spomky-labs.com>
*/
final class ChainAccessTokenExtractor implements AccessTokenExtractorInterface
{
/**
* @param AccessTokenExtractorInterface[] $accessTokenExtractors
*/
public function __construct(
private readonly iterable $accessTokenExtractors,
) {
}
public function extractAccessToken(Request $request): ?string
{
foreach ($this->accessTokenExtractors as $extractor) {
if ($accessToken = $extractor->extractAccessToken($request)) {
return $accessToken;
}
}
return null;
}
}
@@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\AccessToken;
use Symfony\Component\HttpFoundation\Request;
/**
* Extracts a token from the body request.
*
* WARNING!
* Because of the security weaknesses associated with this method,
* the request body method SHOULD NOT be used except in application contexts
* where participating browsers do not have access to the "Authorization" request header field.
*
* @author Florent Morselli <florent.morselli@spomky-labs.com>
*
* @see https://datatracker.ietf.org/doc/html/rfc6750#section-2.2
*/
final class FormEncodedBodyExtractor implements AccessTokenExtractorInterface
{
public function __construct(
private readonly string $parameter = 'access_token',
) {
}
public function extractAccessToken(Request $request): ?string
{
if (
Request::METHOD_POST !== $request->getMethod()
|| !str_starts_with($request->headers->get('CONTENT_TYPE', ''), 'application/x-www-form-urlencoded')
) {
return null;
}
$parameter = $request->request->get($this->parameter);
return \is_string($parameter) ? $parameter : null;
}
}
@@ -0,0 +1,49 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\AccessToken;
use Symfony\Component\HttpFoundation\Request;
/**
* Extracts a token from the request header.
*
* @author Florent Morselli <florent.morselli@spomky-labs.com>
*
* @see https://datatracker.ietf.org/doc/html/rfc6750#section-2.1
*/
final class HeaderAccessTokenExtractor implements AccessTokenExtractorInterface
{
private string $regex;
public function __construct(
private readonly string $headerParameter = 'Authorization',
private readonly string $tokenType = 'Bearer',
) {
$this->regex = sprintf(
'/^%s([a-zA-Z0-9\-_\+~\/\.]+=*)$/',
'' === $this->tokenType ? '' : preg_quote($this->tokenType).'\s+'
);
}
public function extractAccessToken(Request $request): ?string
{
if (!$request->headers->has($this->headerParameter) || !\is_string($header = $request->headers->get($this->headerParameter))) {
return null;
}
if (preg_match($this->regex, $header, $matches)) {
return $matches[1];
}
return null;
}
}
@@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\AccessToken\Oidc\Exception;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
/**
* This exception is thrown when the token signature is invalid.
*/
class InvalidSignatureException extends AuthenticationException
{
public function getMessageKey(): string
{
return 'Invalid token signature.';
}
}
@@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\AccessToken\Oidc\Exception;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
/**
* This exception is thrown when the user is invalid on the OIDC server (e.g.: "email" property is not in the scope).
*/
class MissingClaimException extends AuthenticationException
{
public function getMessageKey(): string
{
return 'Missing claim.';
}
}
@@ -0,0 +1,114 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\AccessToken\Oidc;
use Jose\Component\Checker;
use Jose\Component\Checker\ClaimCheckerManager;
use Jose\Component\Core\Algorithm;
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\Core\JWK;
use Jose\Component\Core\JWKSet;
use Jose\Component\Signature\JWSTokenSupport;
use Jose\Component\Signature\JWSVerifier;
use Jose\Component\Signature\Serializer\CompactSerializer;
use Jose\Component\Signature\Serializer\JWSSerializerManager;
use Psr\Clock\ClockInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Clock\Clock;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface;
use Symfony\Component\Security\Http\AccessToken\Oidc\Exception\InvalidSignatureException;
use Symfony\Component\Security\Http\AccessToken\Oidc\Exception\MissingClaimException;
use Symfony\Component\Security\Http\Authenticator\FallbackUserLoader;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
/**
* The token handler decodes and validates the token, and retrieves the user identifier from it.
*/
final class OidcTokenHandler implements AccessTokenHandlerInterface
{
use OidcTrait;
public function __construct(
private Algorithm|AlgorithmManager $signatureAlgorithm,
private JWK|JWKSet $jwkset,
private string $audience,
private array $issuers,
private string $claim = 'sub',
private ?LoggerInterface $logger = null,
private ClockInterface $clock = new Clock(),
) {
if ($signatureAlgorithm instanceof Algorithm) {
trigger_deprecation('symfony/security-http', '7.1', 'First argument must be instance of %s, %s given.', AlgorithmManager::class, Algorithm::class);
$this->signatureAlgorithm = new AlgorithmManager([$signatureAlgorithm]);
}
if ($jwkset instanceof JWK) {
trigger_deprecation('symfony/security-http', '7.1', 'Second argument must be instance of %s, %s given.', JWKSet::class, JWK::class);
$this->jwkset = new JWKSet([$jwkset]);
}
}
public function getUserBadgeFrom(string $accessToken): UserBadge
{
if (!class_exists(JWSVerifier::class) || !class_exists(Checker\HeaderCheckerManager::class)) {
throw new \LogicException('You cannot use the "oidc" token handler since "web-token/jwt-signature" and "web-token/jwt-checker" are not installed. Try running "composer require web-token/jwt-signature web-token/jwt-checker".');
}
try {
// Decode the token
$jwsVerifier = new JWSVerifier($this->signatureAlgorithm);
$serializerManager = new JWSSerializerManager([new CompactSerializer()]);
$jws = $serializerManager->unserialize($accessToken);
$claims = json_decode($jws->getPayload(), true);
// Verify the signature
if (!$jwsVerifier->verifyWithKeySet($jws, $this->jwkset, 0)) {
throw new InvalidSignatureException();
}
// Verify the headers
$headerCheckerManager = new Checker\HeaderCheckerManager([
new Checker\AlgorithmChecker($this->signatureAlgorithm->list()),
], [
new JWSTokenSupport(),
]);
// if this check fails, an InvalidHeaderException is thrown
$headerCheckerManager->check($jws, 0);
// Verify the claims
$checkers = [
new Checker\IssuedAtChecker(clock: $this->clock, allowedTimeDrift: 0, protectedHeaderOnly: false),
new Checker\NotBeforeChecker(clock: $this->clock, allowedTimeDrift: 0, protectedHeaderOnly: false),
new Checker\ExpirationTimeChecker(clock: $this->clock, allowedTimeDrift: 0, protectedHeaderOnly: false),
new Checker\AudienceChecker($this->audience),
new Checker\IssuerChecker($this->issuers),
];
$claimCheckerManager = new ClaimCheckerManager($checkers);
// if this check fails, an InvalidClaimException is thrown
$claimCheckerManager->check($claims);
if (empty($claims[$this->claim])) {
throw new MissingClaimException(sprintf('"%s" claim not found.', $this->claim));
}
// UserLoader argument can be overridden by a UserProvider on AccessTokenAuthenticator::authenticate
return new UserBadge($claims[$this->claim], new FallbackUserLoader(fn () => $this->createUser($claims)), $claims);
} catch (\Exception $e) {
$this->logger?->error('An error occurred while decoding and validating the token.', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
throw new BadCredentialsException('Invalid credentials.', $e->getCode(), $e);
}
}
}
@@ -0,0 +1,53 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\AccessToken\Oidc;
use Symfony\Component\Security\Core\User\OidcUser;
use function Symfony\Component\String\u;
/**
* Creates {@see OidcUser} from claims.
*
* @internal
*/
trait OidcTrait
{
private function createUser(array $claims): OidcUser
{
if (!\function_exists('Symfony\Component\String\u')) {
throw new \LogicException('You cannot use the "OidcUserInfoTokenHandler" since the String component is not installed. Try running "composer require symfony/string".');
}
foreach ($claims as $claim => $value) {
unset($claims[$claim]);
if ('' === $value || null === $value) {
continue;
}
$claims[u($claim)->camel()->toString()] = $value;
}
if (isset($claims['updatedAt']) && '' !== $claims['updatedAt']) {
$claims['updatedAt'] = (new \DateTimeImmutable())->setTimestamp($claims['updatedAt']);
}
if (\array_key_exists('emailVerified', $claims) && null !== $claims['emailVerified'] && '' !== $claims['emailVerified']) {
$claims['emailVerified'] = (bool) $claims['emailVerified'];
}
if (\array_key_exists('phoneNumberVerified', $claims) && null !== $claims['phoneNumberVerified'] && '' !== $claims['phoneNumberVerified']) {
$claims['phoneNumberVerified'] = (bool) $claims['phoneNumberVerified'];
}
return new OidcUser(...$claims);
}
}
@@ -0,0 +1,60 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\AccessToken\Oidc;
use Psr\Log\LoggerInterface;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface;
use Symfony\Component\Security\Http\AccessToken\Oidc\Exception\MissingClaimException;
use Symfony\Component\Security\Http\Authenticator\FallbackUserLoader;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
* The token handler validates the token on the OIDC server and retrieves the user identifier.
*/
final class OidcUserInfoTokenHandler implements AccessTokenHandlerInterface
{
use OidcTrait;
public function __construct(
private HttpClientInterface $client,
private ?LoggerInterface $logger = null,
private string $claim = 'sub',
) {
}
public function getUserBadgeFrom(string $accessToken): UserBadge
{
try {
// Call the OIDC server to retrieve the user info
// If the token is invalid or expired, the OIDC server will return an error
$claims = $this->client->request('GET', '', [
'auth_bearer' => $accessToken,
])->toArray();
if (empty($claims[$this->claim])) {
throw new MissingClaimException(sprintf('"%s" claim not found on OIDC server response.', $this->claim));
}
// UserLoader argument can be overridden by a UserProvider on AccessTokenAuthenticator::authenticate
return new UserBadge($claims[$this->claim], new FallbackUserLoader(fn () => $this->createUser($claims)), $claims);
} catch (\Exception $e) {
$this->logger?->error('An error occurred on OIDC server.', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
throw new BadCredentialsException('Invalid credentials.', $e->getCode(), $e);
}
}
}
@@ -0,0 +1,44 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\AccessToken;
use Symfony\Component\HttpFoundation\Request;
/**
* Extracts a token from a query string parameter.
*
* WARNING!
* Because of the security weaknesses associated with the URI method,
* including the high likelihood that the URL containing the access token will be logged,
* it SHOULD NOT be used unless it is impossible to transport the access token in the
* request header field.
*
* @author Florent Morselli <florent.morselli@spomky-labs.com>
*
* @see https://datatracker.ietf.org/doc/html/rfc6750#section-2.3
*/
final class QueryAccessTokenExtractor implements AccessTokenExtractorInterface
{
public const PARAMETER = 'access_token';
public function __construct(
private readonly string $parameter = self::PARAMETER,
) {
}
public function extractAccessToken(Request $request): ?string
{
$parameter = $request->query->get($this->parameter);
return \is_string($parameter) ? $parameter : null;
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Attribute;
use Symfony\Component\HttpKernel\Attribute\ValueResolver;
use Symfony\Component\Security\Http\Controller\UserValueResolver;
/**
* Indicates that a controller argument should receive the current logged user.
*/
#[\Attribute(\Attribute::TARGET_PARAMETER)]
class CurrentUser extends ValueResolver
{
/**
* @param bool $disabled Whether this value resolver is disabled, which allows to enable a value resolver globally while disabling it in specific cases
* @param string $resolver The class name of the resolver to use
*/
public function __construct(bool $disabled = false, string $resolver = UserValueResolver::class)
{
parent::__construct($resolver, $disabled);
}
}
@@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Attribute;
use Symfony\Component\ExpressionLanguage\Expression;
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION)]
final class IsCsrfTokenValid
{
public function __construct(
/**
* Sets the id, or an Expression evaluated to the id, used when generating the token.
*/
public string|Expression $id,
/**
* Sets the key of the request that contains the actual token value that should be validated.
*/
public ?string $tokenKey = '_token',
) {
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Attribute;
use Symfony\Component\ExpressionLanguage\Expression;
/**
* Checks if user has permission to access to some resource using security roles and voters.
*
* @see https://symfony.com/doc/current/security.html#roles
*
* @author Ryan Weaver <ryan@knpuniversity.com>
*/
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION)]
final class IsGranted
{
/**
* @param string|Expression $attribute The attribute that will be checked against a given authentication token and optional subject
* @param array|string|Expression|null $subject An optional subject - e.g. the current object being voted on
* @param string|null $message A custom message when access is not granted
* @param int|null $statusCode If set, will throw HttpKernel's HttpException with the given $statusCode; if null, Security\Core's AccessDeniedException will be used
* @param int|null $exceptionCode If set, will add the exception code to thrown exception
*/
public function __construct(
public string|Expression $attribute,
public array|string|Expression|null $subject = null,
public ?string $message = null,
public ?int $statusCode = null,
public ?int $exceptionCode = null,
) {
}
}
@@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authentication;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
/**
* Interface for custom authentication failure handlers.
*
* If you want to customize the failure handling process, instead of
* overwriting the respective listener globally, you can set a custom failure
* handler which implements this interface.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
interface AuthenticationFailureHandlerInterface
{
/**
* This is called when an interactive authentication attempt fails.
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response;
}
@@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authentication;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
/**
* Interface for a custom authentication success handler.
*
* If you want to customize the success handling process, instead of
* overwriting the respective listener globally, you can set a custom success
* handler which implements this interface.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
interface AuthenticationSuccessHandlerInterface
{
/**
* Usually called by AuthenticatorInterface::onAuthenticationSuccess() implementations.
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token): ?Response;
}
@@ -0,0 +1,75 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authentication;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\SecurityRequestAttributes;
/**
* Extracts Security Errors from Request.
*
* @author Boris Vujicic <boris.vujicic@gmail.com>
*/
class AuthenticationUtils
{
private RequestStack $requestStack;
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
public function getLastAuthenticationError(bool $clearSession = true): ?AuthenticationException
{
$request = $this->getRequest();
$authenticationException = null;
if ($request->attributes->has(SecurityRequestAttributes::AUTHENTICATION_ERROR)) {
$authenticationException = $request->attributes->get(SecurityRequestAttributes::AUTHENTICATION_ERROR);
} elseif ($request->hasSession() && ($session = $request->getSession())->has(SecurityRequestAttributes::AUTHENTICATION_ERROR)) {
$authenticationException = $session->get(SecurityRequestAttributes::AUTHENTICATION_ERROR);
if ($clearSession) {
$session->remove(SecurityRequestAttributes::AUTHENTICATION_ERROR);
}
}
return $authenticationException;
}
public function getLastUsername(): string
{
$request = $this->getRequest();
if ($request->attributes->has(SecurityRequestAttributes::LAST_USERNAME)) {
return $request->attributes->get(SecurityRequestAttributes::LAST_USERNAME) ?? '';
}
return $request->hasSession() ? ($request->getSession()->get(SecurityRequestAttributes::LAST_USERNAME) ?? '') : '';
}
/**
* @throws \LogicException
*/
private function getRequest(): Request
{
$request = $this->requestStack->getCurrentRequest();
if (null === $request) {
throw new \LogicException('Request should exist so it can be processed for error.');
}
return $request;
}
}
@@ -0,0 +1,269 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authentication;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\AuthenticationEvents;
use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;
use Symfony\Component\Security\Core\Exception\AccountStatusException;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
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;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
use Symfony\Component\Security\Http\Event\AuthenticationTokenCreatedEvent;
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\Event\LoginFailureEvent;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
use Symfony\Component\Security\Http\SecurityEvents;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* @author Wouter de Jong <wouter@wouterj.nl>
* @author Ryan Weaver <ryan@symfonycasts.com>
* @author Amaury Leroux de Lens <amaury@lerouxdelens.com>
*/
class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthenticatorInterface
{
private iterable $authenticators;
private TokenStorageInterface $tokenStorage;
private EventDispatcherInterface $eventDispatcher;
private bool $eraseCredentials;
private ?LoggerInterface $logger;
private string $firewallName;
private bool $hideUserNotFoundExceptions;
private array $requiredBadges;
/**
* @param iterable<mixed, AuthenticatorInterface> $authenticators
*/
public function __construct(iterable $authenticators, TokenStorageInterface $tokenStorage, EventDispatcherInterface $eventDispatcher, string $firewallName, ?LoggerInterface $logger = null, bool $eraseCredentials = true, bool $hideUserNotFoundExceptions = true, array $requiredBadges = [])
{
$this->authenticators = $authenticators;
$this->tokenStorage = $tokenStorage;
$this->eventDispatcher = $eventDispatcher;
$this->firewallName = $firewallName;
$this->logger = $logger;
$this->eraseCredentials = $eraseCredentials;
$this->hideUserNotFoundExceptions = $hideUserNotFoundExceptions;
$this->requiredBadges = $requiredBadges;
}
/**
* @param BadgeInterface[] $badges Optionally, pass some Passport badges to use for the manual login
*/
public function authenticateUser(UserInterface $user, AuthenticatorInterface $authenticator, Request $request, array $badges = []): ?Response
{
// create an authentication token for the User
$passport = new SelfValidatingPassport(new UserBadge($user->getUserIdentifier(), fn () => $user), $badges);
$token = $authenticator->createToken($passport, $this->firewallName);
// announce the authentication token
$token = $this->eventDispatcher->dispatch(new AuthenticationTokenCreatedEvent($token, $passport))->getAuthenticatedToken();
// authenticate this in the system
return $this->handleAuthenticationSuccess($token, $passport, $request, $authenticator, $this->tokenStorage->getToken());
}
public function supports(Request $request): ?bool
{
if (null !== $this->logger) {
$context = ['firewall_name' => $this->firewallName];
if (is_countable($this->authenticators)) {
$context['authenticators'] = \count($this->authenticators);
}
$this->logger->debug('Checking for authenticator support.', $context);
}
$authenticators = [];
$skippedAuthenticators = [];
$lazy = true;
foreach ($this->authenticators as $authenticator) {
$this->logger?->debug('Checking support on authenticator.', ['firewall_name' => $this->firewallName, 'authenticator' => $authenticator::class]);
if (!$authenticator instanceof AuthenticatorInterface) {
throw new \InvalidArgumentException(sprintf('Authenticator "%s" must implement "%s".', get_debug_type($authenticator), AuthenticatorInterface::class));
}
if (false !== $supports = $authenticator->supports($request)) {
$authenticators[] = $authenticator;
$lazy = $lazy && null === $supports;
} else {
$this->logger?->debug('Authenticator does not support the request.', ['firewall_name' => $this->firewallName, 'authenticator' => $authenticator::class]);
$skippedAuthenticators[] = $authenticator;
}
}
if (!$authenticators) {
return false;
}
$request->attributes->set('_security_authenticators', $authenticators);
$request->attributes->set('_security_skipped_authenticators', $skippedAuthenticators);
return $lazy ? null : true;
}
public function authenticateRequest(Request $request): ?Response
{
$authenticators = $request->attributes->get('_security_authenticators');
$request->attributes->remove('_security_authenticators');
$request->attributes->remove('_security_skipped_authenticators');
if (!$authenticators) {
return null;
}
return $this->executeAuthenticators($authenticators, $request);
}
/**
* @param AuthenticatorInterface[] $authenticators
*/
private function executeAuthenticators(array $authenticators, Request $request): ?Response
{
foreach ($authenticators as $authenticator) {
// recheck if the authenticator still supports the listener. supports() is called
// eagerly (before token storage is initialized), whereas authenticate() is called
// lazily (after initialization).
if (false === $authenticator->supports($request)) {
$this->logger?->debug('Skipping the "{authenticator}" authenticator as it did not support the request.', ['authenticator' => ($authenticator instanceof TraceableAuthenticator ? $authenticator->getAuthenticator() : $authenticator)::class]);
continue;
}
$response = $this->executeAuthenticator($authenticator, $request);
if (null !== $response) {
$this->logger?->debug('The "{authenticator}" authenticator set the response. Any later authenticator will not be called', ['authenticator' => ($authenticator instanceof TraceableAuthenticator ? $authenticator->getAuthenticator() : $authenticator)::class]);
return $response;
}
}
return null;
}
private function executeAuthenticator(AuthenticatorInterface $authenticator, Request $request): ?Response
{
$passport = null;
$previousToken = $this->tokenStorage->getToken();
try {
// get the passport from the Authenticator
$passport = $authenticator->authenticate($request);
// check the passport (e.g. password checking)
$event = new CheckPassportEvent($authenticator, $passport);
$this->eventDispatcher->dispatch($event);
// check if all badges are resolved
$resolvedBadges = [];
foreach ($passport->getBadges() as $badge) {
if (!$badge->isResolved()) {
throw new BadCredentialsException(sprintf('Authentication failed: Security badge "%s" is not resolved, did you forget to register the correct listeners?', get_debug_type($badge)));
}
$resolvedBadges[] = $badge::class;
}
$missingRequiredBadges = array_diff($this->requiredBadges, $resolvedBadges);
if ($missingRequiredBadges) {
throw new BadCredentialsException(sprintf('Authentication failed; Some badges marked as required by the firewall config are not available on the passport: "%s".', implode('", "', $missingRequiredBadges)));
}
// create the authentication token
$authenticatedToken = $authenticator->createToken($passport, $this->firewallName);
// announce the authentication token
$authenticatedToken = $this->eventDispatcher->dispatch(new AuthenticationTokenCreatedEvent($authenticatedToken, $passport))->getAuthenticatedToken();
if (true === $this->eraseCredentials) {
$authenticatedToken->eraseCredentials();
}
$this->eventDispatcher->dispatch(new AuthenticationSuccessEvent($authenticatedToken), AuthenticationEvents::AUTHENTICATION_SUCCESS);
$this->logger?->info('Authenticator successful!', ['token' => $authenticatedToken, 'authenticator' => ($authenticator instanceof TraceableAuthenticator ? $authenticator->getAuthenticator() : $authenticator)::class]);
} catch (AuthenticationException $e) {
// oh no! Authentication failed!
$response = $this->handleAuthenticationFailure($e, $request, $authenticator, $passport);
if ($response instanceof Response) {
return $response;
}
return null;
}
// success! (sets the token on the token storage, etc)
$response = $this->handleAuthenticationSuccess($authenticatedToken, $passport, $request, $authenticator, $previousToken);
if ($response instanceof Response) {
return $response;
}
$this->logger?->debug('Authenticator set no success response: request continues.', ['authenticator' => ($authenticator instanceof TraceableAuthenticator ? $authenticator->getAuthenticator() : $authenticator)::class]);
return null;
}
private function handleAuthenticationSuccess(TokenInterface $authenticatedToken, Passport $passport, Request $request, AuthenticatorInterface $authenticator, ?TokenInterface $previousToken): ?Response
{
$this->tokenStorage->setToken($authenticatedToken);
$response = $authenticator->onAuthenticationSuccess($request, $authenticatedToken, $this->firewallName);
if ($authenticator instanceof InteractiveAuthenticatorInterface && $authenticator->isInteractive()) {
$loginEvent = new InteractiveLoginEvent($request, $authenticatedToken);
$this->eventDispatcher->dispatch($loginEvent, SecurityEvents::INTERACTIVE_LOGIN);
}
$this->eventDispatcher->dispatch($loginSuccessEvent = new LoginSuccessEvent($authenticator, $passport, $authenticatedToken, $request, $response, $this->firewallName, $previousToken));
return $loginSuccessEvent->getResponse();
}
/**
* Handles an authentication failure and returns the Response for the authenticator.
*/
private function handleAuthenticationFailure(AuthenticationException $authenticationException, Request $request, AuthenticatorInterface $authenticator, ?Passport $passport): ?Response
{
$this->logger?->info('Authenticator failed.', ['exception' => $authenticationException, 'authenticator' => ($authenticator instanceof TraceableAuthenticator ? $authenticator->getAuthenticator() : $authenticator)::class]);
// Avoid leaking error details in case of invalid user (e.g. user not found or invalid account status)
// to prevent user enumeration via response content comparison
if ($this->hideUserNotFoundExceptions && ($authenticationException instanceof UserNotFoundException || ($authenticationException instanceof AccountStatusException && !$authenticationException instanceof CustomUserMessageAccountStatusException))) {
$authenticationException = new BadCredentialsException('Bad credentials.', 0, $authenticationException);
}
$response = $authenticator->onAuthenticationFailure($request, $authenticationException);
if (null !== $response && null !== $this->logger) {
$this->logger->debug('The "{authenticator}" authenticator set the failure response.', ['authenticator' => ($authenticator instanceof TraceableAuthenticator ? $authenticator->getAuthenticator() : $authenticator)::class]);
}
$this->eventDispatcher->dispatch($loginFailureEvent = new LoginFailureEvent($authenticationException, $authenticator, $request, $response, $this->firewallName, $passport));
// returning null is ok, it means they want the request to continue
return $loginFailureEvent->getResponse();
}
}
@@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authentication;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface;
/**
* @author Wouter de Jong <wouter@wouterj.nl>
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
interface AuthenticatorManagerInterface
{
/**
* Called to see if authentication should be attempted on this request.
*
* @see FirewallListenerInterface::supports()
*/
public function supports(Request $request): ?bool;
/**
* Tries to authenticate the request and returns a response - if any authenticator set one.
*/
public function authenticateRequest(Request $request): ?Response;
}
@@ -0,0 +1,40 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authentication;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class CustomAuthenticationFailureHandler implements AuthenticationFailureHandlerInterface
{
private AuthenticationFailureHandlerInterface $handler;
/**
* @param array $options Options for processing a successful authentication attempt
*/
public function __construct(AuthenticationFailureHandlerInterface $handler, array $options)
{
$this->handler = $handler;
if (method_exists($handler, 'setOptions')) {
$this->handler->setOptions($options);
}
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
{
return $this->handler->onAuthenticationFailure($request, $exception);
}
}
@@ -0,0 +1,44 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authentication;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface
{
private AuthenticationSuccessHandlerInterface $handler;
/**
* @param array $options Options for processing a successful authentication attempt
*/
public function __construct(AuthenticationSuccessHandlerInterface $handler, array $options, string $firewallName)
{
$this->handler = $handler;
if (method_exists($handler, 'setOptions')) {
$this->handler->setOptions($options);
}
if (method_exists($handler, 'setFirewallName')) {
$this->handler->setFirewallName($firewallName);
}
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token): ?Response
{
return $this->handler->onAuthenticationSuccess($request, $token);
}
}
@@ -0,0 +1,97 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authentication;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Component\Security\Http\ParameterBagUtils;
use Symfony\Component\Security\Http\SecurityRequestAttributes;
/**
* Class with the default authentication failure handling logic.
*
* Can be optionally be extended from by the developer to alter the behavior
* while keeping the default behavior.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
* @author Alexander <iam.asm89@gmail.com>
*/
class DefaultAuthenticationFailureHandler implements AuthenticationFailureHandlerInterface
{
protected HttpKernelInterface $httpKernel;
protected HttpUtils $httpUtils;
protected array $options;
protected ?LoggerInterface $logger;
protected array $defaultOptions = [
'failure_path' => null,
'failure_forward' => false,
'login_path' => '/login',
'failure_path_parameter' => '_failure_path',
];
public function __construct(HttpKernelInterface $httpKernel, HttpUtils $httpUtils, array $options = [], ?LoggerInterface $logger = null)
{
$this->httpKernel = $httpKernel;
$this->httpUtils = $httpUtils;
$this->logger = $logger;
$this->setOptions($options);
}
/**
* Gets the options.
*/
public function getOptions(): array
{
return $this->options;
}
public function setOptions(array $options): void
{
$this->options = array_merge($this->defaultOptions, $options);
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
{
$options = $this->options;
$failureUrl = ParameterBagUtils::getRequestParameterValue($request, $options['failure_path_parameter']);
if (\is_string($failureUrl) && (str_starts_with($failureUrl, '/') || str_starts_with($failureUrl, 'http'))) {
$options['failure_path'] = $failureUrl;
} elseif ($this->logger && $failureUrl) {
$this->logger->debug(sprintf('Ignoring query parameter "%s": not a valid URL.', $options['failure_path_parameter']));
}
$options['failure_path'] ??= $options['login_path'];
if ($options['failure_forward']) {
$this->logger?->debug('Authentication failure, forward triggered.', ['failure_path' => $options['failure_path']]);
$subRequest = $this->httpUtils->createRequest($request, $options['failure_path']);
$subRequest->attributes->set(SecurityRequestAttributes::AUTHENTICATION_ERROR, $exception);
return $this->httpKernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST);
}
$this->logger?->debug('Authentication failure, redirect triggered.', ['failure_path' => $options['failure_path']]);
if (!$request->attributes->getBoolean('_stateless')) {
$request->getSession()->set(SecurityRequestAttributes::AUTHENTICATION_ERROR, $exception);
}
return $this->httpUtils->createRedirectResponse($request, $options['failure_path']);
}
}
@@ -0,0 +1,120 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authentication;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Component\Security\Http\ParameterBagUtils;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
/**
* Class with the default authentication success handling logic.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
* @author Alexander <iam.asm89@gmail.com>
*/
class DefaultAuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface
{
use TargetPathTrait;
protected HttpUtils $httpUtils;
protected array $options;
protected ?LoggerInterface $logger;
protected ?string $firewallName = null;
protected array $defaultOptions = [
'always_use_default_target_path' => false,
'default_target_path' => '/',
'login_path' => '/login',
'target_path_parameter' => '_target_path',
'use_referer' => false,
];
/**
* @param array $options Options for processing a successful authentication attempt
*/
public function __construct(HttpUtils $httpUtils, array $options = [], ?LoggerInterface $logger = null)
{
$this->httpUtils = $httpUtils;
$this->logger = $logger;
$this->setOptions($options);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token): ?Response
{
return $this->httpUtils->createRedirectResponse($request, $this->determineTargetUrl($request));
}
/**
* Gets the options.
*/
public function getOptions(): array
{
return $this->options;
}
public function setOptions(array $options): void
{
$this->options = array_merge($this->defaultOptions, $options);
}
public function getFirewallName(): ?string
{
return $this->firewallName;
}
public function setFirewallName(string $firewallName): void
{
$this->firewallName = $firewallName;
}
/**
* Builds the target URL according to the defined options.
*/
protected function determineTargetUrl(Request $request): string
{
if ($this->options['always_use_default_target_path']) {
return $this->options['default_target_path'];
}
$targetUrl = ParameterBagUtils::getRequestParameterValue($request, $this->options['target_path_parameter']);
if (\is_string($targetUrl) && (str_starts_with($targetUrl, '/') || str_starts_with($targetUrl, 'http'))) {
return $targetUrl;
}
if ($this->logger && $targetUrl) {
$this->logger->debug(sprintf('Ignoring query parameter "%s": not a valid URL.', $this->options['target_path_parameter']));
}
$firewallName = $this->getFirewallName();
if (null !== $firewallName && !$request->attributes->getBoolean('_stateless') && $targetUrl = $this->getTargetPath($request->getSession(), $firewallName)) {
$this->removeTargetPath($request->getSession(), $firewallName);
return $targetUrl;
}
if ($this->options['use_referer'] && $targetUrl = $request->headers->get('Referer')) {
if (false !== $pos = strpos($targetUrl, '?')) {
$targetUrl = substr($targetUrl, 0, $pos);
}
if ($targetUrl && $targetUrl !== $this->httpUtils->generateUri($request, $this->options['login_path'])) {
return $targetUrl;
}
}
return $this->options['default_target_path'];
}
}
@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authentication;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
/**
* @author Wouter de Jong <wouter@wouterj.nl>
*/
interface UserAuthenticatorInterface
{
/**
* Convenience method to programmatically login a user and return a
* Response *if any* for success.
*
* @param BadgeInterface[] $badges Optionally, pass some Passport badges to use for the manual login
*/
public function authenticateUser(UserInterface $user, AuthenticatorInterface $authenticator, Request $request, array $badges = []): ?Response;
}
@@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authenticator;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken;
/**
* An optional base class that creates the necessary tokens for you.
*
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
abstract class AbstractAuthenticator implements AuthenticatorInterface
{
/**
* Shortcut to create a PostAuthenticationToken for you, if you don't really
* care about which authenticated token you're using.
*/
public function createToken(Passport $passport, string $firewallName): TokenInterface
{
return new PostAuthenticationToken($passport->getUser(), $firewallName, $passport->getUser()->getRoles());
}
}
@@ -0,0 +1,74 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authenticator;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
use Symfony\Component\Security\Http\SecurityRequestAttributes;
/**
* A base class to make form login authentication easier!
*
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
abstract class AbstractLoginFormAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface, InteractiveAuthenticatorInterface
{
/**
* Return the URL to the login page.
*/
abstract protected function getLoginUrl(Request $request): string;
/**
* Override to change the request conditions that have to be
* matched in order to handle the login form submit.
*
* This default implementation handles all POST requests to the
* login path (@see getLoginUrl()).
*/
public function supports(Request $request): bool
{
return $request->isMethod('POST') && $this->getLoginUrl($request) === $request->getBaseUrl().$request->getPathInfo();
}
/**
* Override to change what happens after a bad username/password is submitted.
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
{
if ($request->hasSession()) {
$request->getSession()->set(SecurityRequestAttributes::AUTHENTICATION_ERROR, $exception);
}
$url = $this->getLoginUrl($request);
return new RedirectResponse($url);
}
/**
* Override to control what happens when the user hits a secure page
* but isn't logged in yet.
*/
public function start(Request $request, ?AuthenticationException $authException = null): Response
{
$url = $this->getLoginUrl($request);
return new RedirectResponse($url);
}
public function isInteractive(): bool
{
return true;
}
}
@@ -0,0 +1,130 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authenticator;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PreAuthenticatedUserBadge;
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;
/**
* The base authenticator for authenticators to use pre-authenticated
* requests (e.g. using certificates).
*
* @author Wouter de Jong <wouter@wouterj.nl>
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
abstract class AbstractPreAuthenticatedAuthenticator implements InteractiveAuthenticatorInterface
{
private UserProviderInterface $userProvider;
private TokenStorageInterface $tokenStorage;
private string $firewallName;
private ?LoggerInterface $logger;
public function __construct(UserProviderInterface $userProvider, TokenStorageInterface $tokenStorage, string $firewallName, ?LoggerInterface $logger = null)
{
$this->userProvider = $userProvider;
$this->tokenStorage = $tokenStorage;
$this->firewallName = $firewallName;
$this->logger = $logger;
}
/**
* Returns the username of the pre-authenticated user.
*
* This authenticator is skipped if null is returned or a custom
* BadCredentialsException is thrown.
*/
abstract protected function extractUsername(Request $request): ?string;
public function supports(Request $request): ?bool
{
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]);
return false;
}
if (null === $username) {
$this->logger?->debug('Skipping pre-authenticated authenticator no username could be extracted.', ['authenticator' => static::class]);
return false;
}
// do not overwrite already stored tokens from the same user (i.e. from the session)
$token = $this->tokenStorage->getToken();
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]);
return false;
}
$request->attributes->set('_pre_authenticated_username', $username);
return true;
}
public function authenticate(Request $request): Passport
{
$userBadge = new UserBadge($request->attributes->get('_pre_authenticated_username'), $this->userProvider->loadUserByIdentifier(...));
return new SelfValidatingPassport($userBadge, [new PreAuthenticatedUserBadge()]);
}
public function createToken(Passport $passport, string $firewallName): TokenInterface
{
return new PreAuthenticatedToken($passport->getUser(), $firewallName, $passport->getUser()->getRoles());
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return null; // let the original request continue
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$this->clearToken($exception);
return null;
}
public function isInteractive(): bool
{
return true;
}
private function clearToken(AuthenticationException $exception): void
{
$token = $this->tokenStorage->getToken();
if ($token instanceof PreAuthenticatedToken && $this->firewallName === $token->getFirewallName()) {
$this->tokenStorage->setToken(null);
$this->logger?->info('Cleared pre-authenticated token due to an exception.', ['exception' => $exception]);
}
}
}
@@ -0,0 +1,123 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\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\BadCredentialsException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\AccessToken\AccessTokenExtractorInterface;
use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
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\Contracts\Translation\TranslatorInterface;
/**
* Provides an implementation of the RFC6750 of an authentication via
* an access token.
*
* @author Florent Morselli <florent.morselli@spomky-labs.com>
*/
class AccessTokenAuthenticator implements AuthenticatorInterface
{
private ?TranslatorInterface $translator = null;
public function __construct(
private readonly AccessTokenHandlerInterface $accessTokenHandler,
private readonly AccessTokenExtractorInterface $accessTokenExtractor,
private readonly ?UserProviderInterface $userProvider = null,
private readonly ?AuthenticationSuccessHandlerInterface $successHandler = null,
private readonly ?AuthenticationFailureHandlerInterface $failureHandler = null,
private readonly ?string $realm = null,
) {
}
public function supports(Request $request): ?bool
{
return null === $this->accessTokenExtractor->extractAccessToken($request) ? false : null;
}
public function authenticate(Request $request): Passport
{
$accessToken = $this->accessTokenExtractor->extractAccessToken($request);
if (!$accessToken) {
throw new BadCredentialsException('Invalid credentials.');
}
$userBadge = $this->accessTokenHandler->getUserBadgeFrom($accessToken);
if ($this->userProvider && (null === $userBadge->getUserLoader() || $userBadge->getUserLoader() instanceof FallbackUserLoader)) {
$userBadge->setUserLoader($this->userProvider->loadUserByIdentifier(...));
}
return new SelfValidatingPassport($userBadge);
}
public function createToken(Passport $passport, string $firewallName): TokenInterface
{
return new PostAuthenticationToken($passport->getUser(), $firewallName, $passport->getUser()->getRoles());
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return $this->successHandler?->onAuthenticationSuccess($request, $token);
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
{
if (null !== $this->failureHandler) {
return $this->failureHandler->onAuthenticationFailure($request, $exception);
}
if (null !== $this->translator) {
$errorMessage = $this->translator->trans($exception->getMessageKey(), $exception->getMessageData(), 'security');
} else {
$errorMessage = strtr($exception->getMessageKey(), $exception->getMessageData());
}
return new Response(
null,
Response::HTTP_UNAUTHORIZED,
['WWW-Authenticate' => $this->getAuthenticateHeader($errorMessage)]
);
}
public function setTranslator(?TranslatorInterface $translator): void
{
$this->translator = $translator;
}
/**
* @see https://datatracker.ietf.org/doc/html/rfc6750#section-3
*/
private function getAuthenticateHeader(?string $errorDescription = null): string
{
$data = [
'realm' => $this->realm,
'error' => 'invalid_token',
'error_description' => $errorDescription,
];
$values = [];
foreach ($data as $k => $v) {
if (null === $v || '' === $v) {
continue;
}
$values[] = sprintf('%s="%s"', $k, $v);
}
return sprintf('Bearer %s', implode(',', $values));
}
}
@@ -0,0 +1,87 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\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\Http\Authenticator\Passport\Passport;
/**
* The interface for all authenticators.
*
* @author Ryan Weaver <ryan@symfonycasts.com>
* @author Amaury Leroux de Lens <amaury@lerouxdelens.com>
* @author Wouter de Jong <wouter@wouterj.nl>
*/
interface AuthenticatorInterface
{
/**
* Does the authenticator support the given Request?
*
* 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.
*/
public function supports(Request $request): ?bool;
/**
* Create a passport for the current request.
*
* The passport contains the user, credentials and any additional information
* that has to be checked by the Symfony Security system. For example, a login
* form authenticator will probably return a passport containing the user, the
* presented password and the CSRF token value.
*
* You may throw any AuthenticationException in this method in case of error (e.g.
* a UserNotFoundException when the user cannot be found).
*
* @throws AuthenticationException
*/
public function authenticate(Request $request): Passport;
/**
* Create an authenticated token for the given user.
*
* If you don't care about which token class is used or don't really
* understand what a "token" is, you can skip this method by extending
* the AbstractAuthenticator class from your authenticator.
*
* @see AbstractAuthenticator
*
* @param Passport $passport The passport returned from authenticate()
*/
public function createToken(Passport $passport, string $firewallName): TokenInterface;
/**
* Called when authentication executed and was successful!
*
* This should return the Response sent back to the user, like a
* RedirectResponse to the last page they visited.
*
* If you return null, the current request will continue, and the user
* will be authenticated. This makes sense, for example, with an API.
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response;
/**
* Called when authentication executed, but failed (e.g. wrong username password).
*
* This should return the Response sent back to the user, like a
* RedirectResponse to the login page or a 403 response.
*
* If you return null, the request will continue, but the user will
* not be authenticated. This is probably not what you want to do.
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response;
}
@@ -0,0 +1,118 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authenticator\Debug;
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\Http\Authenticator\AuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
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\VarDumper\Caster\ClassStub;
/**
* Collects info about an authenticator for debugging purposes.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
final class TraceableAuthenticator implements AuthenticatorInterface, InteractiveAuthenticatorInterface, AuthenticationEntryPointInterface
{
private ?Passport $passport = null;
private ?float $duration = null;
private ClassStub|string $stub;
private ?bool $authenticated = null;
public function __construct(private AuthenticatorInterface $authenticator)
{
}
public function getInfo(): array
{
return [
'supports' => true,
'passport' => $this->passport,
'duration' => $this->duration,
'stub' => $this->stub ??= class_exists(ClassStub::class) ? new ClassStub($this->authenticator::class) : $this->authenticator::class,
'authenticated' => $this->authenticated,
'badges' => array_map(
static function (BadgeInterface $badge): array {
return [
'stub' => class_exists(ClassStub::class) ? new ClassStub($badge::class) : $badge::class,
'resolved' => $badge->isResolved(),
];
},
$this->passport?->getBadges() ?? [],
),
];
}
public function supports(Request $request): ?bool
{
return $this->authenticator->supports($request);
}
public function authenticate(Request $request): Passport
{
$startTime = microtime(true);
$this->passport = $this->authenticator->authenticate($request);
$this->duration = microtime(true) - $startTime;
return $this->passport;
}
public function createToken(Passport $passport, string $firewallName): TokenInterface
{
return $this->authenticator->createToken($passport, $firewallName);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
$this->authenticated = true;
return $this->authenticator->onAuthenticationSuccess($request, $token, $firewallName);
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$this->authenticated = false;
return $this->authenticator->onAuthenticationFailure($request, $exception);
}
public function start(Request $request, ?AuthenticationException $authException = null): Response
{
if (!$this->authenticator instanceof AuthenticationEntryPointInterface) {
throw new NotAnEntryPointException();
}
return $this->authenticator->start($request, $authException);
}
public function isInteractive(): bool
{
return $this->authenticator instanceof InteractiveAuthenticatorInterface && $this->authenticator->isInteractive();
}
public function getAuthenticator(): AuthenticatorInterface
{
return $this->authenticator;
}
public function __call($method, $args): mixed
{
return $this->authenticator->{$method}(...$args);
}
}
@@ -0,0 +1,89 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authenticator\Debug;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Http\Firewall\AbstractListener;
use Symfony\Component\Security\Http\Firewall\AuthenticatorManagerListener;
use Symfony\Component\VarDumper\Caster\ClassStub;
use Symfony\Contracts\Service\ResetInterface;
/**
* Decorates the AuthenticatorManagerListener to collect information about security authenticators.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
final class TraceableAuthenticatorManagerListener extends AbstractListener implements ResetInterface
{
private AuthenticatorManagerListener $authenticationManagerListener;
private array $authenticatorsInfo = [];
private bool $hasVardumper;
public function __construct(AuthenticatorManagerListener $authenticationManagerListener)
{
$this->authenticationManagerListener = $authenticationManagerListener;
$this->hasVardumper = class_exists(ClassStub::class);
}
public function supports(Request $request): ?bool
{
return $this->authenticationManagerListener->supports($request);
}
public function authenticate(RequestEvent $event): void
{
$request = $event->getRequest();
if (!$authenticators = $request->attributes->get('_security_authenticators')) {
return;
}
foreach ($request->attributes->get('_security_skipped_authenticators') as $skippedAuthenticator) {
$this->authenticatorsInfo[] = [
'supports' => false,
'stub' => $this->hasVardumper ? new ClassStub($skippedAuthenticator::class) : $skippedAuthenticator::class,
'passport' => null,
'duration' => 0,
'authenticated' => null,
'badges' => [],
];
}
foreach ($authenticators as $key => $authenticator) {
$authenticators[$key] = new TraceableAuthenticator($authenticator);
}
$request->attributes->set('_security_authenticators', $authenticators);
$this->authenticationManagerListener->authenticate($event);
foreach ($authenticators as $authenticator) {
$this->authenticatorsInfo[] = $authenticator->getInfo();
}
}
public function getAuthenticatorManagerListener(): AuthenticatorManagerListener
{
return $this->authenticationManagerListener;
}
public function getAuthenticatorsInfo(): array
{
return $this->authenticatorsInfo;
}
public function reset(): void
{
$this->authenticatorsInfo = [];
}
}
@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authenticator;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* This wrapper serves as a marker interface to indicate badge user loaders that should not be overridden by the
* default user provider.
*
* @internal
*/
final class FallbackUserLoader
{
public function __construct(private $inner)
{
}
public function __invoke(mixed ...$args): ?UserInterface
{
return ($this->inner)(...$args);
}
}
@@ -0,0 +1,173 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authenticator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
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\Badge\UserBadge;
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\ParameterBagUtils;
use Symfony\Component\Security\Http\SecurityRequestAttributes;
/**
* @author Wouter de Jong <wouter@wouterj.nl>
* @author Fabien Potencier <fabien@symfony.com>
*
* @final
*/
class FormLoginAuthenticator extends AbstractLoginFormAuthenticator
{
private HttpUtils $httpUtils;
private UserProviderInterface $userProvider;
private AuthenticationSuccessHandlerInterface $successHandler;
private AuthenticationFailureHandlerInterface $failureHandler;
private array $options;
private HttpKernelInterface $httpKernel;
public function __construct(HttpUtils $httpUtils, UserProviderInterface $userProvider, AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, array $options)
{
$this->httpUtils = $httpUtils;
$this->userProvider = $userProvider;
$this->successHandler = $successHandler;
$this->failureHandler = $failureHandler;
$this->options = array_merge([
'username_parameter' => '_username',
'password_parameter' => '_password',
'check_path' => '/login_check',
'post_only' => true,
'form_only' => false,
'enable_csrf' => false,
'csrf_parameter' => '_csrf_token',
'csrf_token_id' => 'authenticate',
], $options);
}
protected function getLoginUrl(Request $request): string
{
return $this->httpUtils->generateUri($request, $this->options['login_path']);
}
public function supports(Request $request): 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);
}
public function authenticate(Request $request): Passport
{
$credentials = $this->getCredentials($request);
$userBadge = new UserBadge($credentials['username'], $this->userProvider->loadUserByIdentifier(...));
$passport = new Passport($userBadge, new PasswordCredentials($credentials['password']), [new RememberMeBadge()]);
if ($this->options['enable_csrf']) {
$passport->addBadge(new CsrfTokenBadge($this->options['csrf_token_id'], $credentials['csrf_token']));
}
if ($this->userProvider instanceof PasswordUpgraderInterface) {
$passport->addBadge(new PasswordUpgradeBadge($credentials['password'], $this->userProvider));
}
return $passport;
}
public function createToken(Passport $passport, string $firewallName): TokenInterface
{
return new UsernamePasswordToken($passport->getUser(), $firewallName, $passport->getUser()->getRoles());
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return $this->successHandler->onAuthenticationSuccess($request, $token);
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
{
return $this->failureHandler->onAuthenticationFailure($request, $exception);
}
private function getCredentials(Request $request): array
{
$credentials = [];
$credentials['csrf_token'] = ParameterBagUtils::getRequestParameterValue($request, $this->options['csrf_parameter']);
if ($this->options['post_only']) {
$credentials['username'] = ParameterBagUtils::getParameterBagValue($request->request, $this->options['username_parameter']);
$credentials['password'] = ParameterBagUtils::getParameterBagValue($request->request, $this->options['password_parameter']) ?? '';
} else {
$credentials['username'] = ParameterBagUtils::getRequestParameterValue($request, $this->options['username_parameter']);
$credentials['password'] = ParameterBagUtils::getRequestParameterValue($request, $this->options['password_parameter']) ?? '';
}
if (!\is_string($credentials['username']) && !$credentials['username'] instanceof \Stringable) {
throw new BadRequestHttpException(sprintf('The key "%s" must be a string, "%s" given.', $this->options['username_parameter'], \gettype($credentials['username'])));
}
$credentials['username'] = trim($credentials['username']);
if ('' === $credentials['username']) {
throw new BadCredentialsException(sprintf('The key "%s" must be a non-empty string.', $this->options['username_parameter']));
}
$request->getSession()->set(SecurityRequestAttributes::LAST_USERNAME, $credentials['username']);
if (!\is_string($credentials['password']) && (!\is_object($credentials['password']) || !method_exists($credentials['password'], '__toString'))) {
throw new BadRequestHttpException(sprintf('The key "%s" must be a string, "%s" given.', $this->options['password_parameter'], \gettype($credentials['password'])));
}
if ('' === (string) $credentials['password']) {
throw new BadCredentialsException(sprintf('The key "%s" must be a non-empty string.', $this->options['password_parameter']));
}
if (!\is_string($credentials['csrf_token'] ?? '') && (!\is_object($credentials['csrf_token']) || !method_exists($credentials['csrf_token'], '__toString'))) {
throw new BadRequestHttpException(sprintf('The key "%s" must be a string, "%s" given.', $this->options['csrf_parameter'], \gettype($credentials['csrf_token'])));
}
return $credentials;
}
public function setHttpKernel(HttpKernelInterface $httpKernel): void
{
$this->httpKernel = $httpKernel;
}
public function start(Request $request, ?AuthenticationException $authException = null): Response
{
if (!$this->options['use_forward']) {
return parent::start($request, $authException);
}
$subRequest = $this->httpUtils->createRequest($request, $this->options['login_path']);
$response = $this->httpKernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST);
if (200 === $response->getStatusCode()) {
$response->setStatusCode(401);
}
return $response;
}
}
@@ -0,0 +1,92 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authenticator;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
/**
* @author Wouter de Jong <wouter@wouterj.nl>
* @author Fabien Potencier <fabien@symfony.com>
*
* @final
*/
class HttpBasicAuthenticator implements AuthenticatorInterface, AuthenticationEntryPointInterface
{
private string $realmName;
private UserProviderInterface $userProvider;
private ?LoggerInterface $logger;
public function __construct(string $realmName, UserProviderInterface $userProvider, ?LoggerInterface $logger = null)
{
$this->realmName = $realmName;
$this->userProvider = $userProvider;
$this->logger = $logger;
}
public function start(Request $request, ?AuthenticationException $authException = null): Response
{
$response = new Response();
$response->headers->set('WWW-Authenticate', sprintf('Basic realm="%s"', $this->realmName));
$response->setStatusCode(401);
return $response;
}
public function supports(Request $request): ?bool
{
return $request->headers->has('PHP_AUTH_USER');
}
public function authenticate(Request $request): Passport
{
$username = $request->headers->get('PHP_AUTH_USER');
$password = $request->headers->get('PHP_AUTH_PW', '');
$userBadge = new UserBadge($username, $this->userProvider->loadUserByIdentifier(...));
$passport = new Passport($userBadge, new PasswordCredentials($password));
if ($this->userProvider instanceof PasswordUpgraderInterface) {
$passport->addBadge(new PasswordUpgradeBadge($password, $this->userProvider));
}
return $passport;
}
public function createToken(Passport $passport, string $firewallName): TokenInterface
{
return new UsernamePasswordToken($passport->getUser(), $firewallName, $passport->getUser()->getRoles());
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$this->logger?->info('Basic authentication failed for user.', ['username' => $request->headers->get('PHP_AUTH_USER'), 'exception' => $exception]);
return $this->start($request, $exception);
}
}
@@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authenticator;
/**
* This is an extension of the authenticator interface that may
* be used by interactive authenticators.
*
* Interactive login requires explicit user action (e.g. a login
* form). Implementing this interface will dispatch the InteractiveLoginEvent
* upon successful login.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
interface InteractiveAuthenticatorInterface extends AuthenticatorInterface
{
/**
* Should return true to make this authenticator perform
* an interactive login.
*/
public function isInteractive(): bool;
}
@@ -0,0 +1,171 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authenticator;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\PropertyAccess\Exception\AccessException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
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\Badge\UserBadge;
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\Contracts\Translation\TranslatorInterface;
/**
* Provides a stateless implementation of an authentication via
* a JSON document composed of a username and a password.
*
* @author Kévin Dunglas <dunglas@gmail.com>
* @author Wouter de Jong <wouter@wouterj.nl>
*
* @final
*/
class JsonLoginAuthenticator implements InteractiveAuthenticatorInterface
{
private array $options;
private HttpUtils $httpUtils;
private UserProviderInterface $userProvider;
private PropertyAccessorInterface $propertyAccessor;
private ?AuthenticationSuccessHandlerInterface $successHandler;
private ?AuthenticationFailureHandlerInterface $failureHandler;
private ?TranslatorInterface $translator = null;
public function __construct(HttpUtils $httpUtils, UserProviderInterface $userProvider, ?AuthenticationSuccessHandlerInterface $successHandler = null, ?AuthenticationFailureHandlerInterface $failureHandler = null, array $options = [], ?PropertyAccessorInterface $propertyAccessor = null)
{
$this->options = array_merge(['username_path' => 'username', 'password_path' => 'password'], $options);
$this->httpUtils = $httpUtils;
$this->successHandler = $successHandler;
$this->failureHandler = $failureHandler;
$this->userProvider = $userProvider;
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
}
public function supports(Request $request): ?bool
{
if (
!str_contains($request->getRequestFormat() ?? '', 'json')
&& !str_contains($request->getContentTypeFormat() ?? '', 'json')
) {
return false;
}
if (isset($this->options['check_path']) && !$this->httpUtils->checkRequestPath($request, $this->options['check_path'])) {
return false;
}
return true;
}
public function authenticate(Request $request): Passport
{
try {
$data = json_decode($request->getContent());
if (!$data instanceof \stdClass) {
throw new BadRequestHttpException('Invalid JSON.');
}
$credentials = $this->getCredentials($data);
} catch (BadRequestHttpException $e) {
$request->setRequestFormat('json');
throw $e;
}
$userBadge = new UserBadge($credentials['username'], $this->userProvider->loadUserByIdentifier(...));
$passport = new Passport($userBadge, new PasswordCredentials($credentials['password']), [new RememberMeBadge((array) $data)]);
if ($this->userProvider instanceof PasswordUpgraderInterface) {
$passport->addBadge(new PasswordUpgradeBadge($credentials['password'], $this->userProvider));
}
return $passport;
}
public function createToken(Passport $passport, string $firewallName): TokenInterface
{
return new UsernamePasswordToken($passport->getUser(), $firewallName, $passport->getUser()->getRoles());
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
if (null === $this->successHandler) {
return null; // let the original request continue
}
return $this->successHandler->onAuthenticationSuccess($request, $token);
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
if (null === $this->failureHandler) {
if (null !== $this->translator) {
$errorMessage = $this->translator->trans($exception->getMessageKey(), $exception->getMessageData(), 'security');
} else {
$errorMessage = strtr($exception->getMessageKey(), $exception->getMessageData());
}
return new JsonResponse(['error' => $errorMessage], JsonResponse::HTTP_UNAUTHORIZED);
}
return $this->failureHandler->onAuthenticationFailure($request, $exception);
}
public function isInteractive(): bool
{
return true;
}
public function setTranslator(TranslatorInterface $translator): void
{
$this->translator = $translator;
}
private function getCredentials(\stdClass $data): array
{
$credentials = [];
try {
$credentials['username'] = $this->propertyAccessor->getValue($data, $this->options['username_path']);
if (!\is_string($credentials['username']) || '' === $credentials['username']) {
throw new BadRequestHttpException(sprintf('The key "%s" must be a non-empty string.', $this->options['username_path']));
}
} catch (AccessException $e) {
throw new BadRequestHttpException(sprintf('The key "%s" must be provided.', $this->options['username_path']), $e);
}
try {
$credentials['password'] = $this->propertyAccessor->getValue($data, $this->options['password_path']);
$this->propertyAccessor->setValue($data, $this->options['password_path'], null);
if (!\is_string($credentials['password']) || '' === $credentials['password']) {
throw new BadRequestHttpException(sprintf('The key "%s" must be a non-empty string.', $this->options['password_path']));
}
} catch (AccessException $e) {
throw new BadRequestHttpException(sprintf('The key "%s" must be provided.', $this->options['password_path']), $e);
}
return $credentials;
}
}
@@ -0,0 +1,88 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\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\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
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\HttpUtils;
use Symfony\Component\Security\Http\LoginLink\Exception\InvalidLoginLinkAuthenticationException;
use Symfony\Component\Security\Http\LoginLink\Exception\InvalidLoginLinkExceptionInterface;
use Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface;
/**
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
final class LoginLinkAuthenticator extends AbstractAuthenticator implements InteractiveAuthenticatorInterface
{
private LoginLinkHandlerInterface $loginLinkHandler;
private HttpUtils $httpUtils;
private AuthenticationSuccessHandlerInterface $successHandler;
private AuthenticationFailureHandlerInterface $failureHandler;
private array $options;
public function __construct(LoginLinkHandlerInterface $loginLinkHandler, HttpUtils $httpUtils, AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, array $options)
{
$this->loginLinkHandler = $loginLinkHandler;
$this->httpUtils = $httpUtils;
$this->successHandler = $successHandler;
$this->failureHandler = $failureHandler;
$this->options = $options + ['check_post_only' => false];
}
public function supports(Request $request): ?bool
{
return ($this->options['check_post_only'] ? $request->isMethod('POST') : true)
&& $this->httpUtils->checkRequestPath($request, $this->options['check_route']);
}
public function authenticate(Request $request): Passport
{
if (!$username = $request->get('user')) {
throw new InvalidLoginLinkAuthenticationException('Missing user from link.');
}
$userBadge = new UserBadge($username, function () use ($request) {
try {
$user = $this->loginLinkHandler->consumeLoginLink($request);
} catch (InvalidLoginLinkExceptionInterface $e) {
throw new InvalidLoginLinkAuthenticationException('Login link could not be validated.', 0, $e);
}
return $user;
});
return new SelfValidatingPassport($userBadge, [new RememberMeBadge()]);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return $this->successHandler->onAuthenticationSuccess($request, $token);
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
{
return $this->failureHandler->onAuthenticationFailure($request, $exception);
}
public function isInteractive(): bool
{
return true;
}
}
@@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authenticator\Passport\Badge;
/**
* Passport badges allow to add more information to a passport (e.g. a CSRF token).
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
interface BadgeInterface
{
/**
* Checks if this badge is resolved by the security system.
*
* After authentication, all badges must return `true` in this method in order
* for the authentication to succeed.
*/
public function isResolved(): bool;
}
@@ -0,0 +1,64 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authenticator\Passport\Badge;
use Symfony\Component\Security\Http\EventListener\CsrfProtectionListener;
/**
* Adds automatic CSRF tokens checking capabilities to this authenticator.
*
* @see CsrfProtectionListener
*
* @author Wouter de Jong <wouter@wouterj.nl>
*
* @final
*/
class CsrfTokenBadge implements BadgeInterface
{
private bool $resolved = false;
private string $csrfTokenId;
private ?string $csrfToken;
/**
* @param string $csrfTokenId An arbitrary string used to generate the value of the CSRF token.
* Using a different string for each authenticator improves its security.
* @param string|null $csrfToken The CSRF token presented in the request, if any
*/
public function __construct(string $csrfTokenId, #[\SensitiveParameter] ?string $csrfToken)
{
$this->csrfTokenId = $csrfTokenId;
$this->csrfToken = $csrfToken;
}
public function getCsrfTokenId(): string
{
return $this->csrfTokenId;
}
public function getCsrfToken(): ?string
{
return $this->csrfToken;
}
/**
* @internal
*/
public function markResolved(): void
{
$this->resolved = true;
}
public function isResolved(): bool
{
return $this->resolved;
}
}
@@ -0,0 +1,62 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authenticator\Passport\Badge;
use Symfony\Component\Security\Core\Exception\LogicException;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
/**
* Adds automatic password migration, if enabled and required in the password encoder.
*
* @see PasswordUpgraderInterface
*
* @author Wouter de Jong <wouter@wouterj.nl>
*
* @final
*/
class PasswordUpgradeBadge implements BadgeInterface
{
private ?string $plaintextPassword = null;
private ?PasswordUpgraderInterface $passwordUpgrader;
/**
* @param string $plaintextPassword The presented password, used in the rehash
* @param PasswordUpgraderInterface|null $passwordUpgrader The password upgrader, defaults to the UserProvider if null
*/
public function __construct(#[\SensitiveParameter] string $plaintextPassword, ?PasswordUpgraderInterface $passwordUpgrader = null)
{
$this->plaintextPassword = $plaintextPassword;
$this->passwordUpgrader = $passwordUpgrader;
}
public function getAndErasePlaintextPassword(): string
{
$password = $this->plaintextPassword;
if (null === $password) {
throw new LogicException('The password is erased as another listener already used this badge.');
}
$this->plaintextPassword = null;
return $password;
}
public function getPasswordUpgrader(): ?PasswordUpgraderInterface
{
return $this->passwordUpgrader;
}
public function isResolved(): bool
{
return true;
}
}
@@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authenticator\Passport\Badge;
use Symfony\Component\Security\Http\Authenticator\AbstractPreAuthenticatedAuthenticator;
/**
* Marks the authentication as being pre-authenticated.
*
* This disables pre-authentication user checkers.
*
* @see AbstractPreAuthenticatedAuthenticator
*
* @author Wouter de Jong <wouter@wouterj.nl>
*
* @final
*/
class PreAuthenticatedUserBadge implements BadgeInterface
{
public function isResolved(): bool
{
return true;
}
}
@@ -0,0 +1,76 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authenticator\Passport\Badge;
use Symfony\Component\Security\Http\EventListener\CheckRememberMeConditionsListener;
/**
* Adds support for remember me to this authenticator.
*
* The presence of this badge doesn't create the remember-me cookie. The actual
* cookie is only created if this badge is enabled. By default, this is done
* by the {@see CheckRememberMeConditionsListener} if all conditions are met.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*
* @final
*/
class RememberMeBadge implements BadgeInterface
{
private bool $enabled = false;
public function __construct(
public readonly array $parameters = [],
) {
}
/**
* Enables remember-me cookie creation.
*
* In most cases, {@see CheckRememberMeConditionsListener} enables this
* automatically if always_remember_me is true or the remember_me_parameter
* exists in the request.
*
* @return $this
*/
public function enable(): static
{
$this->enabled = true;
return $this;
}
/**
* Disables remember-me cookie creation.
*
* The default is disabled, this can be called to suppress creation
* after it was enabled.
*
* @return $this
*/
public function disable(): static
{
$this->enabled = false;
return $this;
}
public function isEnabled(): bool
{
return $this->enabled;
}
public function isResolved(): bool
{
return true; // remember me does not need to be explicitly resolved
}
}
@@ -0,0 +1,121 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authenticator\Passport\Badge;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\AuthenticationServiceException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\EventListener\UserProviderListener;
/**
* Represents the user in the authentication process.
*
* It uses an identifier (e.g. email, or username) and
* "user loader" to load the related User object.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
class UserBadge implements BadgeInterface
{
public const MAX_USERNAME_LENGTH = 4096;
private string $userIdentifier;
/** @var callable|null */
private $userLoader;
private UserInterface $user;
private ?array $attributes;
/**
* Initializes the user badge.
*
* You must provide a $userIdentifier. This is a unique string representing the
* user for this authentication (e.g. the email if authentication is done using
* email + password; or a string combining email+company if authentication is done
* based on email *and* company name). This string can be used for e.g. login throttling.
*
* Optionally, you may pass a user loader. This callable receives the $userIdentifier
* as argument and must return a UserInterface object (otherwise an AuthenticationServiceException
* is thrown). If this is not set, the default user provider will be used with
* $userIdentifier as username.
*/
public function __construct(string $userIdentifier, ?callable $userLoader = null, ?array $attributes = null)
{
if (\strlen($userIdentifier) > self::MAX_USERNAME_LENGTH) {
throw new BadCredentialsException('Username too long.');
}
$this->userIdentifier = $userIdentifier;
$this->userLoader = $userLoader;
$this->attributes = $attributes;
}
public function getUserIdentifier(): string
{
return $this->userIdentifier;
}
public function getAttributes(): ?array
{
return $this->attributes;
}
/**
* @throws AuthenticationException when the user cannot be found
*/
public function getUser(): UserInterface
{
if (isset($this->user)) {
return $this->user;
}
if (null === $this->userLoader) {
throw new \LogicException(sprintf('No user loader is configured, did you forget to register the "%s" listener?', UserProviderListener::class));
}
if (null === $this->getAttributes()) {
$user = ($this->userLoader)($this->userIdentifier);
} else {
$user = ($this->userLoader)($this->userIdentifier, $this->getAttributes());
}
// No user has been found via the $this->userLoader callback
if (null === $user) {
$exception = new UserNotFoundException();
$exception->setUserIdentifier($this->userIdentifier);
throw $exception;
}
if (!$user instanceof UserInterface) {
throw new AuthenticationServiceException(sprintf('The user provider must return a UserInterface object, "%s" given.', get_debug_type($user)));
}
return $this->user = $user;
}
public function getUserLoader(): ?callable
{
return $this->userLoader;
}
public function setUserLoader(callable $userLoader): void
{
$this->userLoader = $userLoader;
}
public function isResolved(): bool
{
return true;
}
}
@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authenticator\Passport\Credentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
/**
* Credentials are a special badge used to explicitly mark the
* credential check of an authenticator.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
interface CredentialsInterface extends BadgeInterface
{
}
@@ -0,0 +1,56 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authenticator\Passport\Credentials;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* Implements credentials checking using a custom checker function.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*
* @final
*/
class CustomCredentials implements CredentialsInterface
{
private \Closure $customCredentialsChecker;
private mixed $credentials;
private bool $resolved = false;
/**
* @param callable $customCredentialsChecker the check function. If this function does not return `true`, a
* BadCredentialsException is thrown. You may also throw a more
* specific exception in the function.
*/
public function __construct(callable $customCredentialsChecker, mixed $credentials)
{
$this->customCredentialsChecker = $customCredentialsChecker(...);
$this->credentials = $credentials;
}
public function executeCustomChecker(UserInterface $user): void
{
$checker = $this->customCredentialsChecker;
if (true !== $checker($this->credentials, $user)) {
throw new BadCredentialsException('Credentials check failed as the callable passed to CustomCredentials did not return "true".');
}
$this->resolved = true;
}
public function isResolved(): bool
{
return $this->resolved;
}
}
@@ -0,0 +1,58 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authenticator\Passport\Credentials;
use Symfony\Component\Security\Core\Exception\LogicException;
/**
* Implements password credentials.
*
* These plaintext passwords are checked by the UserPasswordHasher during
* authentication.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*
* @final
*/
class PasswordCredentials implements CredentialsInterface
{
private ?string $password = null;
private bool $resolved = false;
public function __construct(#[\SensitiveParameter] string $password)
{
$this->password = $password;
}
public function getPassword(): string
{
if (null === $this->password) {
throw new LogicException('The credentials are erased as another listener already verified these credentials.');
}
return $this->password;
}
/**
* @internal
*/
public function markResolved(): void
{
$this->resolved = true;
$this->password = null;
}
public function isResolved(): bool
{
return $this->resolved;
}
}
@@ -0,0 +1,123 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authenticator\Passport;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CredentialsInterface;
/**
* A Passport contains all security-related information that needs to be
* validated during authentication.
*
* A passport badge can be used to add any additional information to the
* passport.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
class Passport
{
protected UserInterface $user;
private array $badges = [];
private array $attributes = [];
/**
* @param CredentialsInterface $credentials The credentials to check for this authentication, use
* SelfValidatingPassport if no credentials should be checked
* @param BadgeInterface[] $badges
*/
public function __construct(UserBadge $userBadge, CredentialsInterface $credentials, array $badges = [])
{
$this->addBadge($userBadge);
$this->addBadge($credentials);
foreach ($badges as $badge) {
$this->addBadge($badge);
}
}
public function getUser(): UserInterface
{
if (!isset($this->user)) {
if (!$this->hasBadge(UserBadge::class)) {
throw new \LogicException('Cannot get the Security user, no username or UserBadge configured for this passport.');
}
$this->user = $this->getBadge(UserBadge::class)->getUser();
}
return $this->user;
}
/**
* Adds a new security badge.
*
* A passport can hold only one instance of the same security badge.
* This method replaces the current badge if it is already set on this
* passport.
*
* @param string|null $badgeFqcn A FQCN to which the badge should be mapped to.
* This allows replacing a built-in badge by a custom one using
* e.g. addBadge(new MyCustomUserBadge(), UserBadge::class)
*
* @return $this
*/
public function addBadge(BadgeInterface $badge, ?string $badgeFqcn = null): static
{
$badgeFqcn ??= $badge::class;
$this->badges[$badgeFqcn] = $badge;
return $this;
}
public function hasBadge(string $badgeFqcn): bool
{
return isset($this->badges[$badgeFqcn]);
}
/**
* @template TBadge of BadgeInterface
*
* @param class-string<TBadge> $badgeFqcn
*
* @return TBadge|null
*/
public function getBadge(string $badgeFqcn): ?BadgeInterface
{
return $this->badges[$badgeFqcn] ?? null;
}
/**
* @return array<class-string<BadgeInterface>, BadgeInterface>
*/
public function getBadges(): array
{
return $this->badges;
}
public function setAttribute(string $name, mixed $value): void
{
$this->attributes[$name] = $value;
}
public function getAttribute(string $name, mixed $default = null): mixed
{
return $this->attributes[$name] ?? $default;
}
public function getAttributes(): array
{
return $this->attributes;
}
}
@@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authenticator\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
/**
* An implementation used when there are no credentials to be checked (e.g.
* API token authentication).
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
class SelfValidatingPassport extends Passport
{
/**
* @param BadgeInterface[] $badges
*/
public function __construct(UserBadge $userBadge, array $badges = [])
{
$this->addBadge($userBadge);
foreach ($badges as $badge) {
$this->addBadge($badge);
}
}
}
@@ -0,0 +1,129 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authenticator;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CookieTheftException;
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
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\RememberMe\RememberMeDetails;
use Symfony\Component\Security\Http\RememberMe\RememberMeHandlerInterface;
use Symfony\Component\Security\Http\RememberMe\ResponseListener;
/**
* The RememberMe *Authenticator* performs remember me authentication.
*
* This authenticator is executed whenever a user's session
* expired and a remember-me cookie was found. This authenticator
* then "re-authenticates" the user using the information in the
* cookie.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
* @author Wouter de Jong <wouter@wouterj.nl>
*
* @final
*/
class RememberMeAuthenticator implements InteractiveAuthenticatorInterface
{
private RememberMeHandlerInterface $rememberMeHandler;
private string $secret;
private TokenStorageInterface $tokenStorage;
private string $cookieName;
private ?LoggerInterface $logger;
public function __construct(RememberMeHandlerInterface $rememberMeHandler, #[\SensitiveParameter] string $secret, TokenStorageInterface $tokenStorage, string $cookieName, ?LoggerInterface $logger = null)
{
if (!$secret) {
throw new InvalidArgumentException('A non-empty secret is required.');
}
$this->rememberMeHandler = $rememberMeHandler;
$this->secret = $secret;
$this->tokenStorage = $tokenStorage;
$this->cookieName = $cookieName;
$this->logger = $logger;
}
public function supports(Request $request): ?bool
{
// do not overwrite already stored tokens (i.e. from the session)
if (null !== $this->tokenStorage->getToken()) {
return false;
}
if (($cookie = $request->attributes->get(ResponseListener::COOKIE_ATTR_NAME)) && null === $cookie->getValue()) {
return false;
}
if (!$request->cookies->has($this->cookieName) || !\is_scalar($request->cookies->all()[$this->cookieName] ?: null)) {
return false;
}
$this->logger?->debug('Remember-me cookie detected.');
// the `null` return value indicates that this authenticator supports lazy firewalls
return null;
}
public function authenticate(Request $request): Passport
{
if (!$rawCookie = $request->cookies->get($this->cookieName)) {
throw new \LogicException('No remember-me cookie is found.');
}
$rememberMeCookie = RememberMeDetails::fromRawCookie($rawCookie);
$userBadge = new UserBadge($rememberMeCookie->getUserIdentifier(), fn () => $this->rememberMeHandler->consumeRememberMeCookie($rememberMeCookie));
return new SelfValidatingPassport($userBadge);
}
public function createToken(Passport $passport, string $firewallName): TokenInterface
{
return new RememberMeToken($passport->getUser(), $firewallName, $this->secret);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return null; // let the original request continue
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
if (null !== $this->logger) {
if ($exception instanceof UserNotFoundException) {
$this->logger->info('User for remember-me cookie not found.', ['exception' => $exception]);
} elseif ($exception instanceof UnsupportedUserException) {
$this->logger->warning('User class for remember-me cookie not supported.', ['exception' => $exception]);
} elseif (!$exception instanceof CookieTheftException) {
$this->logger->debug('Remember me authentication failed.', ['exception' => $exception]);
}
}
return null;
}
public function isInteractive(): bool
{
return true;
}
}
@@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authenticator;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
/**
* This authenticator authenticates a remote user.
*
* @author Wouter de Jong <wouter@wouterj.nl>
* @author Fabien Potencier <fabien@symfony.com>
* @author Maxime Douailin <maxime.douailin@gmail.com>
*
* @internal
*/
final class RemoteUserAuthenticator extends AbstractPreAuthenticatedAuthenticator
{
private string $userKey;
public function __construct(UserProviderInterface $userProvider, TokenStorageInterface $tokenStorage, string $firewallName, string $userKey = 'REMOTE_USER', ?LoggerInterface $logger = null)
{
parent::__construct($userProvider, $tokenStorage, $firewallName, $logger);
$this->userKey = $userKey;
}
protected function extractUsername(Request $request): ?string
{
if (!$request->server->has($this->userKey)) {
throw new BadCredentialsException(sprintf('User key was not found: "%s".', $this->userKey));
}
return $request->server->get($this->userKey);
}
}
@@ -0,0 +1,62 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authenticator\Token;
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
use Symfony\Component\Security\Core\User\UserInterface;
class PostAuthenticationToken extends AbstractToken
{
private string $firewallName;
/**
* @param string[] $roles An array of roles
*
* @throws \InvalidArgumentException
*/
public function __construct(UserInterface $user, string $firewallName, array $roles)
{
parent::__construct($roles);
if ('' === $firewallName) {
throw new \InvalidArgumentException('$firewallName must not be empty.');
}
$this->setUser($user);
$this->firewallName = $firewallName;
}
/**
* This is meant to be only a token, where credentials
* have already been used and are thus cleared.
*/
public function getCredentials(): mixed
{
return [];
}
public function getFirewallName(): string
{
return $this->firewallName;
}
public function __serialize(): array
{
return [$this->firewallName, parent::__serialize()];
}
public function __unserialize(array $data): void
{
[$this->firewallName, $parentData] = $data;
parent::__unserialize($parentData);
}
}
@@ -0,0 +1,62 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authenticator;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
/**
* This authenticator authenticates pre-authenticated (by the
* webserver) X.509 certificates.
*
* @author Wouter de Jong <wouter@wouterj.nl>
* @author Fabien Potencier <fabien@symfony.com>
*
* @final
*/
class X509Authenticator extends AbstractPreAuthenticatedAuthenticator
{
private string $userKey;
private string $credentialsKey;
private string $credentialUserIdentifier;
public function __construct(UserProviderInterface $userProvider, TokenStorageInterface $tokenStorage, string $firewallName, string $userKey = 'SSL_CLIENT_S_DN_Email', string $credentialsKey = 'SSL_CLIENT_S_DN', ?LoggerInterface $logger = null, string $credentialUserIdentifier = 'emailAddress')
{
parent::__construct($userProvider, $tokenStorage, $firewallName, $logger);
$this->userKey = $userKey;
$this->credentialsKey = $credentialsKey;
$this->credentialUserIdentifier = $credentialUserIdentifier;
}
protected function extractUsername(Request $request): string
{
$username = null;
if ($request->server->has($this->userKey)) {
$username = $request->server->get($this->userKey);
} elseif (
$request->server->has($this->credentialsKey)
&& preg_match('#'.preg_quote($this->credentialUserIdentifier, '#').'=([^,/]++)#', $request->server->get($this->credentialsKey), $matches)
) {
$username = trim($matches[1]);
}
if (null === $username) {
throw new BadCredentialsException(sprintf('SSL credentials not found: "%s", "%s".', $this->userKey, $this->credentialsKey));
}
return $username;
}
}
@@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authorization;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
/**
* This is used by the ExceptionListener to translate an AccessDeniedException
* to a Response object.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
interface AccessDeniedHandlerInterface
{
/**
* Handles an access denied failure.
*/
public function handle(Request $request, AccessDeniedException $accessDeniedException): ?Response;
}
+73
View File
@@ -0,0 +1,73 @@
CHANGELOG
=========
7.1
---
* Add `#[IsCsrfTokenValid]` attribute
* Add CAS 2.0 access token handler
* Make empty username or empty password on form login attempts return Bad Request (400)
7.0
---
* Add argument `$badgeFqcn` to `Passport::addBadge()`
* Add argument `$lifetime` to `LoginLinkHandlerInterface::createLoginLink()`
* Throw when calling the constructor of `DefaultLoginRateLimiter` with an empty secret
6.4
---
* `UserValueResolver` no longer implements `ArgumentValueResolverInterface`
* Deprecate calling the constructor of `DefaultLoginRateLimiter` with an empty secret
6.3
---
* Add `RememberMeBadge` to `JsonLoginAuthenticator` and enable reading parameter in JSON request body
* Add argument `$exceptionCode` to `#[IsGranted]`
* Deprecate passing a secret as the 2nd argument to the constructor of `Symfony\Component\Security\Http\RememberMe\PersistentRememberMeHandler`
* Add `OidcUserInfoTokenHandler` and `OidcTokenHandler` with OIDC support for `AccessTokenAuthenticator`
* Add `attributes` optional array argument in `UserBadge`
* Call `UserBadge::userLoader` with attributes if the argument is set
* Allow to override badge fqcn on `Passport::addBadge`
* Add `SecurityTokenValueResolver` to inject token as controller argument
6.2
---
* Add maximum username length enforcement of 4096 characters in `UserBadge`
* Add `#[IsGranted()]`
* Deprecate empty username or password when using when using `JsonLoginAuthenticator`
* Set custom lifetime for login link
* Add `$lifetime` parameter to `LoginLinkHandlerInterface::createLoginLink()`
* Add RFC6750 Access Token support to allow token-based authentication
* Allow using expressions as `#[IsGranted()]` attribute and subject
6.0
---
* Remove `LogoutSuccessHandlerInterface` and `LogoutHandlerInterface`, register a listener on the `LogoutEvent` event instead
* Remove `CookieClearingLogoutHandler`, `SessionLogoutHandler` and `CsrfTokenClearingLogoutHandler`.
Use `CookieClearingLogoutListener`, `SessionLogoutListener` and `CsrfTokenClearingLogoutListener` instead
5.4
---
* Deprecate the `$authenticationEntryPoint` argument of `ChannelListener`, and add `$httpPort` and `$httpsPort` arguments
* Deprecate `RetryAuthenticationEntryPoint`, this code is now inlined in the `ChannelListener`
* Deprecate `FormAuthenticationEntryPoint` and `BasicAuthenticationEntryPoint`, in the new system the `FormLoginAuthenticator`
and `HttpBasicAuthenticator` should be used instead
* Deprecate `AbstractRememberMeServices`, `PersistentTokenBasedRememberMeServices`, `RememberMeServicesInterface`,
`TokenBasedRememberMeServices`, use the remember me handler alternatives instead
* Deprecate the `$authManager` argument of `AccessListener`
* Deprecate not setting the `$exceptionOnNoToken` argument of `AccessListener` to `false`
* Deprecate `DeauthenticatedEvent`, use `TokenDeauthenticatedEvent` instead
* Deprecate `CookieClearingLogoutHandler`, `SessionLogoutHandler` and `CsrfTokenClearingLogoutHandler`.
Use `CookieClearingLogoutListener`, `SessionLogoutListener` and `CsrfTokenClearingLogoutListener` instead
* Deprecate `PassportInterface`, `UserPassportInterface` and `PassportTrait`, use `Passport` instead
5.3
---
The CHANGELOG for version 5.3 and earlier can be found at https://github.com/symfony/symfony/blob/5.3/src/Symfony/Component/Security/CHANGELOG.md
@@ -0,0 +1,50 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
/**
* @author Konstantin Myakshin <molodchick@gmail.com>
*/
final class SecurityTokenValueResolver implements ValueResolverInterface
{
public function __construct(private readonly TokenStorageInterface $tokenStorage)
{
}
/**
* @return TokenInterface[]
*/
public function resolve(Request $request, ArgumentMetadata $argument): array
{
if (!($type = $argument->getType()) || (TokenInterface::class !== $type && !is_subclass_of($type, TokenInterface::class))) {
return [];
}
if (null !== $token = $this->tokenStorage->getToken()) {
return [$token];
}
if ($argument->isNullable()) {
return [];
}
throw new HttpException(Response::HTTP_UNAUTHORIZED, 'A security token is required but the token storage is empty.');
}
}
@@ -0,0 +1,64 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\Attribute\CurrentUser;
/**
* Supports the argument type of {@see UserInterface}.
*
* @author Iltar van der Berg <kjarli@gmail.com>
*/
final class UserValueResolver implements ValueResolverInterface
{
private TokenStorageInterface $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public function resolve(Request $request, ArgumentMetadata $argument): array
{
// with the attribute, the type can be any UserInterface implementation
// otherwise, the type must be UserInterface
if (UserInterface::class !== $argument->getType() && !$argument->getAttributesOfType(CurrentUser::class, ArgumentMetadata::IS_INSTANCEOF)) {
return [];
}
if (null === $user = $this->tokenStorage->getToken()?->getUser()) {
// if no user is present but a default value exists we use it to prevent the EntityValueResolver or others
// from attempting resolution of the User as the current logged in user was requested here
if ($argument->hasDefaultValue()) {
return [$argument->getDefaultValue()];
}
if (!$argument->isNullable()) {
throw new AccessDeniedException(sprintf('There is no logged-in user to pass to $%s, make the argument nullable if you want to allow anonymous access to the action.', $argument->getName()));
}
return [null];
}
if (null === $argument->getType() || $user instanceof ($argument->getType())) {
return [$user];
}
throw new AccessDeniedException(sprintf('The logged-in user is an instance of "%s" but a user of type "%s" is expected.', $user::class, $argument->getType()));
}
}
@@ -0,0 +1,44 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\EntryPoint;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
/**
* Implement this interface for any classes that will be called to "start"
* the authentication process (see method for more details).
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface AuthenticationEntryPointInterface
{
/**
* Returns a response that directs the user to authenticate.
*
* This is called when an anonymous request accesses a resource that
* requires authentication. The job of this method is to return some
* response that "helps" the user start into the authentication process.
*
* Examples:
*
* - For a form login, you might redirect to the login page
*
* return new RedirectResponse('/login');
*
* - For an API token authentication system, you return a 401 response
*
* return new Response('Auth header required', 401);
*/
public function start(Request $request, ?AuthenticationException $authException = null): Response;
}
@@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\EntryPoint\Exception;
use Symfony\Component\HttpKernel\Attribute\WithHttpStatus;
/**
* Thrown by generic decorators when a decorated authenticator does not implement
* {@see AuthenticationEntryPointInterface}.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
#[WithHttpStatus(401)]
class NotAnEntryPointException extends \RuntimeException
{
}
@@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Event;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Contracts\EventDispatcher\Event;
/**
* When a newly authenticated security token was created, before it becomes effective in the security system.
*
* @author Christian Scheb <me@christianscheb.de>
*/
class AuthenticationTokenCreatedEvent extends Event
{
private TokenInterface $authenticatedToken;
private Passport $passport;
public function __construct(TokenInterface $token, Passport $passport)
{
$this->authenticatedToken = $token;
$this->passport = $passport;
}
public function getAuthenticatedToken(): TokenInterface
{
return $this->authenticatedToken;
}
public function setAuthenticatedToken(TokenInterface $authenticatedToken): void
{
$this->authenticatedToken = $authenticatedToken;
}
public function getPassport(): Passport
{
return $this->passport;
}
}
@@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Event;
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\Debug\TraceableAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Contracts\EventDispatcher\Event;
/**
* This event is dispatched when the credentials have to be checked.
*
* Listeners to this event must validate the user and the
* credentials (e.g. default listeners do password verification and
* user checking)
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
class CheckPassportEvent extends Event
{
private AuthenticatorInterface $authenticator;
private Passport $passport;
public function __construct(AuthenticatorInterface $authenticator, Passport $passport)
{
$this->authenticator = $authenticator;
$this->passport = $passport;
}
public function getAuthenticator(): AuthenticatorInterface
{
return $this->authenticator instanceof TraceableAuthenticator ? $this->authenticator->getAuthenticator() : $this->authenticator;
}
public function getPassport(): Passport
{
return $this->passport;
}
}
@@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Event;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Contracts\EventDispatcher\Event;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class InteractiveLoginEvent extends Event
{
private Request $request;
private TokenInterface $authenticationToken;
public function __construct(Request $request, TokenInterface $authenticationToken)
{
$this->request = $request;
$this->authenticationToken = $authenticationToken;
}
public function getRequest(): Request
{
return $this->request;
}
public function getAuthenticationToken(): TokenInterface
{
return $this->authenticationToken;
}
}
@@ -0,0 +1,61 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Event;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Security\Core\Exception\LazyResponseException;
/**
* Wraps a lazily computed response in a signaling exception.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class LazyResponseEvent extends RequestEvent
{
private parent $event;
public function __construct(parent $event)
{
$this->event = $event;
}
public function setResponse(Response $response): never
{
$this->stopPropagation();
$this->event->stopPropagation();
throw new LazyResponseException($response);
}
public function getKernel(): HttpKernelInterface
{
return $this->event->getKernel();
}
public function getRequest(): Request
{
return $this->event->getRequest();
}
public function getRequestType(): int
{
return $this->event->getRequestType();
}
public function isMainRequest(): bool
{
return $this->event->isMainRequest();
}
}
@@ -0,0 +1,83 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Event;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\Debug\TraceableAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Contracts\EventDispatcher\Event;
/**
* This event is dispatched after an error during authentication.
*
* Listeners to this event can change state based on authentication
* failure (e.g. to implement login throttling).
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
class LoginFailureEvent extends Event
{
private AuthenticationException $exception;
private AuthenticatorInterface $authenticator;
private Request $request;
private ?Response $response;
private string $firewallName;
private ?Passport $passport;
public function __construct(AuthenticationException $exception, AuthenticatorInterface $authenticator, Request $request, ?Response $response, string $firewallName, ?Passport $passport = null)
{
$this->exception = $exception;
$this->authenticator = $authenticator;
$this->request = $request;
$this->response = $response;
$this->firewallName = $firewallName;
$this->passport = $passport;
}
public function getException(): AuthenticationException
{
return $this->exception;
}
public function getAuthenticator(): AuthenticatorInterface
{
return $this->authenticator instanceof TraceableAuthenticator ? $this->authenticator->getAuthenticator() : $this->authenticator;
}
public function getFirewallName(): string
{
return $this->firewallName;
}
public function getRequest(): Request
{
return $this->request;
}
public function setResponse(?Response $response): void
{
$this->response = $response;
}
public function getResponse(): ?Response
{
return $this->response;
}
public function getPassport(): ?Passport
{
return $this->passport;
}
}
@@ -0,0 +1,98 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Event;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
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\Passport\Passport;
use Symfony\Contracts\EventDispatcher\Event;
/**
* This event is dispatched after authentication has successfully completed.
*
* At this stage, the authenticator created a token and
* generated an authentication success response. Listeners to
* this event can do actions related to successful authentication
* (such as migrating the password).
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
class LoginSuccessEvent extends Event
{
private AuthenticatorInterface $authenticator;
private Passport $passport;
private TokenInterface $authenticatedToken;
private ?TokenInterface $previousToken;
private Request $request;
private ?Response $response;
private string $firewallName;
public function __construct(AuthenticatorInterface $authenticator, Passport $passport, TokenInterface $authenticatedToken, Request $request, ?Response $response, string $firewallName, ?TokenInterface $previousToken = null)
{
$this->authenticator = $authenticator;
$this->passport = $passport;
$this->authenticatedToken = $authenticatedToken;
$this->previousToken = $previousToken;
$this->request = $request;
$this->response = $response;
$this->firewallName = $firewallName;
}
public function getAuthenticator(): AuthenticatorInterface
{
return $this->authenticator instanceof TraceableAuthenticator ? $this->authenticator->getAuthenticator() : $this->authenticator;
}
public function getPassport(): Passport
{
return $this->passport;
}
public function getUser(): UserInterface
{
return $this->passport->getUser();
}
public function getAuthenticatedToken(): TokenInterface
{
return $this->authenticatedToken;
}
public function getPreviousToken(): ?TokenInterface
{
return $this->previousToken;
}
public function getRequest(): Request
{
return $this->request;
}
public function getFirewallName(): string
{
return $this->firewallName;
}
public function setResponse(?Response $response): void
{
$this->response = $response;
}
public function getResponse(): ?Response
{
return $this->response;
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Event;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Contracts\EventDispatcher\Event;
/**
* @author Wouter de Jong <wouter@wouterj.nl>
*/
class LogoutEvent extends Event
{
private Request $request;
private ?Response $response = null;
private ?TokenInterface $token;
public function __construct(Request $request, ?TokenInterface $token)
{
$this->request = $request;
$this->token = $token;
}
public function getRequest(): Request
{
return $this->request;
}
public function getToken(): ?TokenInterface
{
return $this->token;
}
public function setResponse(Response $response): void
{
$this->response = $response;
}
public function getResponse(): ?Response
{
return $this->response;
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Event;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Contracts\EventDispatcher\Event;
/**
* SwitchUserEvent.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class SwitchUserEvent extends Event
{
private Request $request;
private UserInterface $targetUser;
private ?TokenInterface $token;
public function __construct(Request $request, UserInterface $targetUser, ?TokenInterface $token = null)
{
$this->request = $request;
$this->targetUser = $targetUser;
$this->token = $token;
}
public function getRequest(): Request
{
return $this->request;
}
public function getTargetUser(): UserInterface
{
return $this->targetUser;
}
public function getToken(): ?TokenInterface
{
return $this->token;
}
public function setToken(TokenInterface $token): void
{
$this->token = $token;
}
}
@@ -0,0 +1,51 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Event;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Contracts\EventDispatcher\Event;
/**
* This event is dispatched when the current security token is deauthenticated
* when trying to reference the token.
*
* This includes changes in the user ({@see DeauthenticatedEvent}), but
* also cases where there is no user provider available to refresh the user.
*
* Use this event if you want to trigger some actions whenever a user is
* deauthenticated and redirected back to the authentication entry point
* (e.g. clearing all remember-me cookies).
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
final class TokenDeauthenticatedEvent extends Event
{
private TokenInterface $originalToken;
private Request $request;
public function __construct(TokenInterface $originalToken, Request $request)
{
$this->originalToken = $originalToken;
$this->request = $request;
}
public function getOriginalToken(): TokenInterface
{
return $this->originalToken;
}
public function getRequest(): Request
{
return $this->request;
}
}
@@ -0,0 +1,98 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\User\LegacyPasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
/**
* This listeners uses the interfaces of authenticators to
* determine how to check credentials.
*
* @author Wouter de Jong <wouter@driveamber.com>
*
* @final
*/
class CheckCredentialsListener implements EventSubscriberInterface
{
private PasswordHasherFactoryInterface $hasherFactory;
public function __construct(PasswordHasherFactoryInterface $hasherFactory)
{
$this->hasherFactory = $hasherFactory;
}
public function checkPassport(CheckPassportEvent $event): void
{
$passport = $event->getPassport();
if ($passport->hasBadge(PasswordCredentials::class)) {
// Use the password hasher to validate the credentials
$user = $passport->getUser();
if (!$user instanceof PasswordAuthenticatedUserInterface) {
throw new \LogicException(sprintf('Class "%s" must implement "%s" for using password-based authentication.', get_debug_type($user), PasswordAuthenticatedUserInterface::class));
}
/** @var PasswordCredentials $badge */
$badge = $passport->getBadge(PasswordCredentials::class);
if ($badge->isResolved()) {
return;
}
$presentedPassword = $badge->getPassword();
if ('' === $presentedPassword) {
throw new BadCredentialsException('The presented password cannot be empty.');
}
if (null === $user->getPassword()) {
throw new BadCredentialsException('The presented password is invalid.');
}
if (!$this->hasherFactory->getPasswordHasher($user)->verify($user->getPassword(), $presentedPassword, $user instanceof LegacyPasswordAuthenticatedUserInterface ? $user->getSalt() : null)) {
throw new BadCredentialsException('The presented password is invalid.');
}
$badge->markResolved();
if (!$passport->hasBadge(PasswordUpgradeBadge::class)) {
$passport->addBadge(new PasswordUpgradeBadge($presentedPassword));
}
return;
}
if ($passport->hasBadge(CustomCredentials::class)) {
/** @var CustomCredentials $badge */
$badge = $passport->getBadge(CustomCredentials::class);
if ($badge->isResolved()) {
return;
}
$badge->executeCustomChecker($passport->getUser());
return;
}
}
public static function getSubscribedEvents(): array
{
return [CheckPassportEvent::class => 'checkPassport'];
}
}
@@ -0,0 +1,72 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\EventListener;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
use Symfony\Component\Security\Http\ParameterBagUtils;
/**
* Checks if all conditions are met for remember me.
*
* The conditions that must be met for this listener to enable remember me:
* A) This badge is present in the Passport
* B) The remember_me key under your firewall is configured
* C) The "remember me" functionality is activated. This is usually
* done by having a _remember_me checkbox in your form, but
* can be configured by the "always_remember_me" and "remember_me_parameter"
* parameters under the "remember_me" firewall key (or "always_remember_me"
* is enabled)
*
* @author Wouter de Jong <wouter@wouterj.nl>
*
* @final
*/
class CheckRememberMeConditionsListener implements EventSubscriberInterface
{
private array $options;
private ?LoggerInterface $logger;
public function __construct(array $options = [], ?LoggerInterface $logger = null)
{
$this->options = $options + ['always_remember_me' => false, 'remember_me_parameter' => '_remember_me'];
$this->logger = $logger;
}
public function onSuccessfulLogin(LoginSuccessEvent $event): void
{
$passport = $event->getPassport();
if (!$passport->hasBadge(RememberMeBadge::class)) {
return;
}
/** @var RememberMeBadge $badge */
$badge = $passport->getBadge(RememberMeBadge::class);
if (!$this->options['always_remember_me']) {
$parameter = ParameterBagUtils::getRequestParameterValue($event->getRequest(), $this->options['remember_me_parameter'], $badge->parameters);
if (!filter_var($parameter, \FILTER_VALIDATE_BOOL)) {
$this->logger?->debug('Remember me disabled; request does not contain remember me parameter ("{parameter}").', ['parameter' => $this->options['remember_me_parameter']]);
return;
}
}
$badge->enable();
}
public static function getSubscribedEvents(): array
{
return [LoginSuccessEvent::class => ['onSuccessfulLogin', -32]];
}
}
@@ -0,0 +1,49 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\Event\LogoutEvent;
/**
* Handler for Clear-Site-Data header during logout.
*
* @author Max Beckers <beckers.maximilian@gmail.com>
*
* @final
*/
class ClearSiteDataLogoutListener implements EventSubscriberInterface
{
private const HEADER_NAME = 'Clear-Site-Data';
/**
* @param string[] $cookieValue The value for the Clear-Site-Data header.
* Can be '*' or a subset of 'cache', 'cookies', 'storage', 'executionContexts'.
*/
public function __construct(private readonly array $cookieValue)
{
}
public function onLogout(LogoutEvent $event): void
{
if (!$event->getResponse()?->headers->has(static::HEADER_NAME)) {
$event->getResponse()->headers->set(static::HEADER_NAME, implode(', ', array_map(fn ($v) => '"'.$v.'"', $this->cookieValue)));
}
}
public static function getSubscribedEvents(): array
{
return [
LogoutEvent::class => 'onLogout',
];
}
}
@@ -0,0 +1,53 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\Event\LogoutEvent;
/**
* This listener clears the passed cookies when a user logs out.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*
* @final
*/
class CookieClearingLogoutListener implements EventSubscriberInterface
{
private array $cookies;
/**
* @param array $cookies An array of cookies (keys are names, values contain path and domain) to unset
*/
public function __construct(array $cookies)
{
$this->cookies = $cookies;
}
public function onLogout(LogoutEvent $event): void
{
if (!$response = $event->getResponse()) {
return;
}
foreach ($this->cookies as $cookieName => $cookieData) {
$response->headers->clearCookie($cookieName, $cookieData['path'], $cookieData['domain'], $cookieData['secure'] ?? false, true, $cookieData['samesite'] ?? null, $cookieData['partitioned'] ?? false);
}
}
public static function getSubscribedEvents(): array
{
return [
LogoutEvent::class => ['onLogout', -255],
];
}
}
@@ -0,0 +1,61 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
/**
* @author Wouter de Jong <wouter@wouterj.nl>
*
* @final
*/
class CsrfProtectionListener implements EventSubscriberInterface
{
private CsrfTokenManagerInterface $csrfTokenManager;
public function __construct(CsrfTokenManagerInterface $csrfTokenManager)
{
$this->csrfTokenManager = $csrfTokenManager;
}
public function checkPassport(CheckPassportEvent $event): void
{
$passport = $event->getPassport();
if (!$passport->hasBadge(CsrfTokenBadge::class)) {
return;
}
/** @var CsrfTokenBadge $badge */
$badge = $passport->getBadge(CsrfTokenBadge::class);
if ($badge->isResolved()) {
return;
}
$csrfToken = new CsrfToken($badge->getCsrfTokenId(), $badge->getCsrfToken());
if (false === $this->csrfTokenManager->isTokenValid($csrfToken)) {
throw new InvalidCsrfTokenException('Invalid CSRF token.');
}
$badge->markResolved();
}
public static function getSubscribedEvents(): array
{
return [CheckPassportEvent::class => ['checkPassport', 512]];
}
}
@@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Csrf\TokenStorage\ClearableTokenStorageInterface;
use Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage;
use Symfony\Component\Security\Http\Event\LogoutEvent;
/**
* @author Christian Flothmann <christian.flothmann@sensiolabs.de>
*
* @final
*/
class CsrfTokenClearingLogoutListener implements EventSubscriberInterface
{
private ClearableTokenStorageInterface $csrfTokenStorage;
public function __construct(ClearableTokenStorageInterface $csrfTokenStorage)
{
$this->csrfTokenStorage = $csrfTokenStorage;
}
public function onLogout(LogoutEvent $event): void
{
if ($this->csrfTokenStorage instanceof SessionTokenStorage && !$event->getRequest()->hasPreviousSession()) {
return;
}
$this->csrfTokenStorage->clear();
}
public static function getSubscribedEvents(): array
{
return [
LogoutEvent::class => 'onLogout',
];
}
}
@@ -0,0 +1,52 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\Event\LogoutEvent;
use Symfony\Component\Security\Http\HttpUtils;
/**
* Default logout listener will redirect users to a configured path.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Alexander <iam.asm89@gmail.com>
*
* @final
*/
class DefaultLogoutListener implements EventSubscriberInterface
{
private HttpUtils $httpUtils;
private string $targetUrl;
public function __construct(HttpUtils $httpUtils, string $targetUrl = '/')
{
$this->httpUtils = $httpUtils;
$this->targetUrl = $targetUrl;
}
public function onLogout(LogoutEvent $event): void
{
if (null !== $event->getResponse()) {
return;
}
$event->setResponse($this->httpUtils->createRedirectResponse($event->getRequest(), $this->targetUrl));
}
public static function getSubscribedEvents(): array
{
return [
LogoutEvent::class => ['onLogout', 64],
];
}
}
@@ -0,0 +1,73 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Http\Attribute\IsCsrfTokenValid;
/**
* Handles the IsCsrfTokenValid attribute on controllers.
*/
final class IsCsrfTokenValidAttributeListener implements EventSubscriberInterface
{
public function __construct(
private readonly CsrfTokenManagerInterface $csrfTokenManager,
private ?ExpressionLanguage $expressionLanguage = null,
) {
}
public function onKernelControllerArguments(ControllerArgumentsEvent $event): void
{
/** @var IsCsrfTokenValid[] $attributes */
if (!\is_array($attributes = $event->getAttributes()[IsCsrfTokenValid::class] ?? null)) {
return;
}
$request = $event->getRequest();
$arguments = $event->getNamedArguments();
foreach ($attributes as $attribute) {
$id = $this->getTokenId($attribute->id, $request, $arguments);
if (!$this->csrfTokenManager->isTokenValid(new CsrfToken($id, $request->getPayload()->getString($attribute->tokenKey)))) {
throw new InvalidCsrfTokenException('Invalid CSRF token.');
}
}
}
public static function getSubscribedEvents(): array
{
return [KernelEvents::CONTROLLER_ARGUMENTS => ['onKernelControllerArguments', 25]];
}
private function getTokenId(string|Expression $id, Request $request, array $arguments): string
{
if (!$id instanceof Expression) {
return $id;
}
$this->expressionLanguage ??= new ExpressionLanguage();
return (string) $this->expressionLanguage->evaluate($id, [
'request' => $request,
'args' => $arguments,
]);
}
}
@@ -0,0 +1,119 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Exception\RuntimeException;
use Symfony\Component\Security\Http\Attribute\IsGranted;
/**
* Handles the IsGranted attribute on controllers.
*
* @author Ryan Weaver <ryan@knpuniversity.com>
*/
class IsGrantedAttributeListener implements EventSubscriberInterface
{
public function __construct(
private readonly AuthorizationCheckerInterface $authChecker,
private ?ExpressionLanguage $expressionLanguage = null,
) {
}
public function onKernelControllerArguments(ControllerArgumentsEvent $event): void
{
/** @var IsGranted[] $attributes */
if (!\is_array($attributes = $event->getAttributes()[IsGranted::class] ?? null)) {
return;
}
$request = $event->getRequest();
$arguments = $event->getNamedArguments();
foreach ($attributes as $attribute) {
$subject = null;
if ($subjectRef = $attribute->subject) {
if (\is_array($subjectRef)) {
foreach ($subjectRef as $refKey => $ref) {
$subject[\is_string($refKey) ? $refKey : (string) $ref] = $this->getIsGrantedSubject($ref, $request, $arguments);
}
} else {
$subject = $this->getIsGrantedSubject($subjectRef, $request, $arguments);
}
}
if (!$this->authChecker->isGranted($attribute->attribute, $subject)) {
$message = $attribute->message ?: sprintf('Access Denied by #[IsGranted(%s)] on controller', $this->getIsGrantedString($attribute));
if ($statusCode = $attribute->statusCode) {
throw new HttpException($statusCode, $message, code: $attribute->exceptionCode ?? 0);
}
$accessDeniedException = new AccessDeniedException($message, code: $attribute->exceptionCode ?? 403);
$accessDeniedException->setAttributes($attribute->attribute);
$accessDeniedException->setSubject($subject);
throw $accessDeniedException;
}
}
}
public static function getSubscribedEvents(): array
{
return [KernelEvents::CONTROLLER_ARGUMENTS => ['onKernelControllerArguments', 20]];
}
private function getIsGrantedSubject(string|Expression $subjectRef, Request $request, array $arguments): mixed
{
if ($subjectRef instanceof Expression) {
$this->expressionLanguage ??= new ExpressionLanguage();
return $this->expressionLanguage->evaluate($subjectRef, [
'request' => $request,
'args' => $arguments,
]);
}
if (!\array_key_exists($subjectRef, $arguments)) {
throw new RuntimeException(sprintf('Could not find the subject "%s" for the #[IsGranted] attribute. Try adding a "$%s" argument to your controller method.', $subjectRef, $subjectRef));
}
return $arguments[$subjectRef];
}
private function getIsGrantedString(IsGranted $isGranted): string
{
$processValue = fn ($value) => sprintf($value instanceof Expression ? 'new Expression("%s")' : '"%s"', $value);
$argsString = $processValue($isGranted->attribute);
if (null !== $subject = $isGranted->subject) {
$subject = !\is_array($subject) ? $processValue($subject) : array_map(function ($key, $value) use ($processValue) {
$value = $processValue($value);
return \is_string($key) ? sprintf('"%s" => %s', $key, $value) : $value;
}, array_keys($subject), $subject);
$argsString .= ', '.(!\is_array($subject) ? $subject : '['.implode(', ', $subject).']');
}
return $argsString;
}
}
@@ -0,0 +1,87 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RateLimiter\PeekableRequestRateLimiterInterface;
use Symfony\Component\HttpFoundation\RateLimiter\RequestRateLimiterInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Exception\TooManyLoginAttemptsAuthenticationException;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
use Symfony\Component\Security\Http\Event\LoginFailureEvent;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
use Symfony\Component\Security\Http\SecurityRequestAttributes;
/**
* @author Wouter de Jong <wouter@wouterj.nl>
*/
final class LoginThrottlingListener implements EventSubscriberInterface
{
private RequestStack $requestStack;
private RequestRateLimiterInterface $limiter;
public function __construct(RequestStack $requestStack, RequestRateLimiterInterface $limiter)
{
$this->requestStack = $requestStack;
$this->limiter = $limiter;
}
public function checkPassport(CheckPassportEvent $event): void
{
$passport = $event->getPassport();
if (!$passport->hasBadge(UserBadge::class)) {
return;
}
$request = $this->requestStack->getMainRequest();
$request->attributes->set(SecurityRequestAttributes::LAST_USERNAME, $passport->getBadge(UserBadge::class)->getUserIdentifier());
if ($this->limiter instanceof PeekableRequestRateLimiterInterface) {
$limit = $this->limiter->peek($request);
// Checking isAccepted here is not enough as peek consumes 0 token, it will
// be accepted even if there are 0 tokens remaining to be consumed. We check both
// anyway for safety in case third party implementations behave unexpectedly.
if (!$limit->isAccepted() || 0 === $limit->getRemainingTokens()) {
throw new TooManyLoginAttemptsAuthenticationException(ceil(($limit->getRetryAfter()->getTimestamp() - time()) / 60));
}
} else {
$limit = $this->limiter->consume($request);
if (!$limit->isAccepted()) {
throw new TooManyLoginAttemptsAuthenticationException(ceil(($limit->getRetryAfter()->getTimestamp() - time()) / 60));
}
}
}
public function onSuccessfulLogin(LoginSuccessEvent $event): void
{
if (!$this->limiter instanceof PeekableRequestRateLimiterInterface) {
$this->limiter->reset($event->getRequest());
}
}
public function onFailedLogin(LoginFailureEvent $event): void
{
if ($this->limiter instanceof PeekableRequestRateLimiterInterface) {
$this->limiter->consume($event->getRequest());
}
}
public static function getSubscribedEvents(): array
{
return [
CheckPassportEvent::class => ['checkPassport', 2080],
LoginFailureEvent::class => 'onFailedLogin',
LoginSuccessEvent::class => 'onSuccessfulLogin',
];
}
}
@@ -0,0 +1,93 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
use Symfony\Component\Security\Core\User\LegacyPasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
/**
* @author Wouter de Jong <wouter@wouterj.nl>
*
* @final
*/
class PasswordMigratingListener implements EventSubscriberInterface
{
private PasswordHasherFactoryInterface $hasherFactory;
public function __construct(PasswordHasherFactoryInterface $hasherFactory)
{
$this->hasherFactory = $hasherFactory;
}
public function onLoginSuccess(LoginSuccessEvent $event): void
{
$passport = $event->getPassport();
if (!$passport->hasBadge(PasswordUpgradeBadge::class)) {
return;
}
/** @var PasswordUpgradeBadge $badge */
$badge = $passport->getBadge(PasswordUpgradeBadge::class);
$plaintextPassword = $badge->getAndErasePlaintextPassword();
if ('' === $plaintextPassword) {
return;
}
$user = $passport->getUser();
if (!$user instanceof PasswordAuthenticatedUserInterface || null === $user->getPassword()) {
return;
}
$passwordHasher = $this->hasherFactory->getPasswordHasher($user);
if (!$passwordHasher->needsRehash($user->getPassword())) {
return;
}
$passwordUpgrader = $badge->getPasswordUpgrader();
if (null === $passwordUpgrader) {
if (!$passport->hasBadge(UserBadge::class)) {
return;
}
/** @var UserBadge $userBadge */
$userBadge = $passport->getBadge(UserBadge::class);
$userLoader = $userBadge->getUserLoader();
if (\is_array($userLoader) && $userLoader[0] instanceof PasswordUpgraderInterface) {
$passwordUpgrader = $userLoader[0];
} elseif (!$userLoader instanceof \Closure
|| !($passwordUpgrader = (new \ReflectionFunction($userLoader))->getClosureThis()) instanceof PasswordUpgraderInterface
) {
return;
}
}
$salt = null;
if ($user instanceof LegacyPasswordAuthenticatedUserInterface) {
$salt = $user->getSalt();
}
$passwordUpgrader->upgradePassword($user, $passwordHasher->hash($plaintextPassword, $salt));
}
public static function getSubscribedEvents(): array
{
return [LoginSuccessEvent::class => 'onLoginSuccess'];
}
}
@@ -0,0 +1,85 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\EventListener;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
use Symfony\Component\Security\Http\Event\LoginFailureEvent;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
use Symfony\Component\Security\Http\Event\LogoutEvent;
use Symfony\Component\Security\Http\Event\TokenDeauthenticatedEvent;
use Symfony\Component\Security\Http\RememberMe\RememberMeHandlerInterface;
/**
* The RememberMe *listener* creates and deletes remember-me cookies.
*
* Upon login success or failure and support for remember me
* in the firewall and authenticator, this listener will create
* a remember-me cookie.
* Upon login failure, all remember-me cookies are removed.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*
* @final
*/
class RememberMeListener implements EventSubscriberInterface
{
private RememberMeHandlerInterface $rememberMeHandler;
private ?LoggerInterface $logger;
public function __construct(RememberMeHandlerInterface $rememberMeHandler, ?LoggerInterface $logger = null)
{
$this->rememberMeHandler = $rememberMeHandler;
$this->logger = $logger;
}
public function onSuccessfulLogin(LoginSuccessEvent $event): void
{
$passport = $event->getPassport();
if (!$passport->hasBadge(RememberMeBadge::class)) {
$this->logger?->debug('Remember me skipped: your authenticator does not support it.', ['authenticator' => $event->getAuthenticator()::class]);
return;
}
// Make sure any old remember-me cookies are cancelled
$this->rememberMeHandler->clearRememberMeCookie();
/** @var RememberMeBadge $badge */
$badge = $passport->getBadge(RememberMeBadge::class);
if (!$badge->isEnabled()) {
$this->logger?->debug('Remember me skipped: the RememberMeBadge is not enabled.');
return;
}
$this->logger?->debug('Remember-me was requested; setting cookie.');
$this->rememberMeHandler->createRememberMeCookie($event->getUser());
}
public function clearCookie(): void
{
$this->rememberMeHandler->clearRememberMeCookie();
}
public static function getSubscribedEvents(): array
{
return [
LoginSuccessEvent::class => ['onSuccessfulLogin', -64],
LoginFailureEvent::class => 'clearCookie',
LogoutEvent::class => 'clearCookie',
TokenDeauthenticatedEvent::class => 'clearCookie',
];
}
}
@@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\Event\LogoutEvent;
/**
* Handler for clearing invalidating the current session.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*
* @final
*/
class SessionLogoutListener implements EventSubscriberInterface
{
public function onLogout(LogoutEvent $event): void
{
if ($event->getRequest()->hasSession()) {
$event->getRequest()->getSession()->invalidate();
}
}
public static function getSubscribedEvents(): array
{
return [
LogoutEvent::class => 'onLogout',
];
}
}
@@ -0,0 +1,62 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy;
use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface;
/**
* Migrates/invalidates the session after successful login.
*
* This should be registered as subscriber to any "stateful" firewalls.
*
* @see SessionAuthenticationStrategy
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
class SessionStrategyListener implements EventSubscriberInterface
{
private SessionAuthenticationStrategyInterface $sessionAuthenticationStrategy;
public function __construct(SessionAuthenticationStrategyInterface $sessionAuthenticationStrategy)
{
$this->sessionAuthenticationStrategy = $sessionAuthenticationStrategy;
}
public function onSuccessfulLogin(LoginSuccessEvent $event): void
{
$request = $event->getRequest();
$token = $event->getAuthenticatedToken();
if (!$request->hasPreviousSession()) {
return;
}
if ($previousToken = $event->getPreviousToken()) {
$user = $token->getUserIdentifier();
$previousUser = $previousToken->getUserIdentifier();
if ('' !== ($user ?? '') && $user === $previousUser && $token::class === $previousToken::class) {
return;
}
}
$this->sessionAuthenticationStrategy->onAuthentication($request, $token);
}
public static function getSubscribedEvents(): array
{
return [LoginSuccessEvent::class => 'onSuccessfulLogin'];
}
}
@@ -0,0 +1,62 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PreAuthenticatedUserBadge;
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
/**
* @author Wouter de Jong <wouter@wouterj.nl>
*
* @final
*/
class UserCheckerListener implements EventSubscriberInterface
{
private UserCheckerInterface $userChecker;
public function __construct(UserCheckerInterface $userChecker)
{
$this->userChecker = $userChecker;
}
public function preCheckCredentials(CheckPassportEvent $event): void
{
$passport = $event->getPassport();
if ($passport->hasBadge(PreAuthenticatedUserBadge::class)) {
return;
}
$this->userChecker->checkPreAuth($passport->getUser());
}
public function postCheckCredentials(AuthenticationSuccessEvent $event): void
{
$user = $event->getAuthenticationToken()->getUser();
if (!$user instanceof UserInterface) {
return;
}
$this->userChecker->checkPostAuth($user);
}
public static function getSubscribedEvents(): array
{
return [
CheckPassportEvent::class => ['preCheckCredentials', 256],
AuthenticationSuccessEvent::class => ['postCheckCredentials', 256],
];
}
}
@@ -0,0 +1,50 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\EventListener;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
/**
* Configures the user provider as user loader, if no user load
* has been explicitly set.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*
* @final
*/
class UserProviderListener
{
private UserProviderInterface $userProvider;
public function __construct(UserProviderInterface $userProvider)
{
$this->userProvider = $userProvider;
}
public function checkPassport(CheckPassportEvent $event): void
{
$passport = $event->getPassport();
if (!$passport->hasBadge(UserBadge::class)) {
return;
}
/** @var UserBadge $badge */
$badge = $passport->getBadge(UserBadge::class);
if (null !== $badge->getUserLoader()) {
return;
}
$badge->setUserLoader($this->userProvider->loadUserByIdentifier(...));
}
}
+140
View File
@@ -0,0 +1,140 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\FinishRequestEvent;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Http\Firewall\ExceptionListener;
use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* Firewall uses a FirewallMap to register security listeners for the given
* request.
*
* It allows for different security strategies within the same application
* (a Basic authentication for the /api, and a web based authentication for
* everything else for instance).
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class Firewall implements EventSubscriberInterface
{
private FirewallMapInterface $map;
private EventDispatcherInterface $dispatcher;
/**
* @var \SplObjectStorage<Request, ExceptionListener>
*/
private \SplObjectStorage $exceptionListeners;
public function __construct(FirewallMapInterface $map, EventDispatcherInterface $dispatcher)
{
$this->map = $map;
$this->dispatcher = $dispatcher;
$this->exceptionListeners = new \SplObjectStorage();
}
/**
* @return void
*/
public function onKernelRequest(RequestEvent $event)
{
if (!$event->isMainRequest()) {
return;
}
// register listeners for this firewall
$listeners = $this->map->getListeners($event->getRequest());
$authenticationListeners = $listeners[0];
$exceptionListener = $listeners[1];
$logoutListener = $listeners[2];
if (null !== $exceptionListener) {
$this->exceptionListeners[$event->getRequest()] = $exceptionListener;
$exceptionListener->register($this->dispatcher);
}
// Authentication listeners are pre-sorted by SortFirewallListenersPass
$authenticationListeners = function () use ($authenticationListeners, $logoutListener) {
if (null !== $logoutListener) {
$logoutListenerPriority = $this->getListenerPriority($logoutListener);
}
foreach ($authenticationListeners as $listener) {
$listenerPriority = $this->getListenerPriority($listener);
// Yielding the LogoutListener at the correct position
if (null !== $logoutListener && $listenerPriority < $logoutListenerPriority) {
yield $logoutListener;
$logoutListener = null;
}
yield $listener;
}
// When LogoutListener has the lowest priority of all listeners
if (null !== $logoutListener) {
yield $logoutListener;
}
};
$this->callListeners($event, $authenticationListeners());
}
/**
* @return void
*/
public function onKernelFinishRequest(FinishRequestEvent $event)
{
$request = $event->getRequest();
if (isset($this->exceptionListeners[$request])) {
$this->exceptionListeners[$request]->unregister($this->dispatcher);
unset($this->exceptionListeners[$request]);
}
}
/**
* @return array
*/
public static function getSubscribedEvents()
{
return [
KernelEvents::REQUEST => ['onKernelRequest', 8],
KernelEvents::FINISH_REQUEST => 'onKernelFinishRequest',
];
}
/**
* @return void
*/
protected function callListeners(RequestEvent $event, iterable $listeners)
{
foreach ($listeners as $listener) {
$listener($event);
if ($event->hasResponse()) {
break;
}
}
}
private function getListenerPriority(object $logoutListener): int
{
return $logoutListener instanceof FirewallListenerInterface ? $logoutListener->getPriority() : 0;
}
}
@@ -0,0 +1,34 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Firewall;
use Symfony\Component\HttpKernel\Event\RequestEvent;
/**
* A base class for listeners that can tell whether they should authenticate incoming requests.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
abstract class AbstractListener implements FirewallListenerInterface
{
final public function __invoke(RequestEvent $event): void
{
if (false !== $this->supports($event->getRequest())) {
$this->authenticate($event);
}
}
public static function getPriority(): int
{
return 0; // Default
}
}
@@ -0,0 +1,98 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Firewall;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Core\Authentication\Token\NullToken;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Http\AccessMapInterface;
use Symfony\Component\Security\Http\Event\LazyResponseEvent;
/**
* AccessListener enforces access control rules.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @final
*/
class AccessListener extends AbstractListener
{
private TokenStorageInterface $tokenStorage;
private AccessDecisionManagerInterface $accessDecisionManager;
private AccessMapInterface $map;
public function __construct(TokenStorageInterface $tokenStorage, AccessDecisionManagerInterface $accessDecisionManager, AccessMapInterface $map, bool $exceptionOnNoToken = false)
{
if (false !== $exceptionOnNoToken) {
throw new \LogicException(sprintf('Argument $exceptionOnNoToken of "%s()" must be set to "false".', __METHOD__));
}
$this->tokenStorage = $tokenStorage;
$this->accessDecisionManager = $accessDecisionManager;
$this->map = $map;
}
public function supports(Request $request): ?bool
{
[$attributes] = $this->map->getPatterns($request);
$request->attributes->set('_access_control_attributes', $attributes);
if ($attributes && [AuthenticatedVoter::PUBLIC_ACCESS] !== $attributes) {
return true;
}
return null;
}
/**
* Handles access authorization.
*
* @throws AccessDeniedException
*/
public function authenticate(RequestEvent $event): void
{
$request = $event->getRequest();
$attributes = $request->attributes->get('_access_control_attributes');
$request->attributes->remove('_access_control_attributes');
if (!$attributes || (
[AuthenticatedVoter::PUBLIC_ACCESS] === $attributes && $event instanceof LazyResponseEvent
)) {
return;
}
$token = $this->tokenStorage->getToken() ?? new NullToken();
if (!$this->accessDecisionManager->decide($token, $attributes, $request, true)) {
throw $this->createAccessDeniedException($request, $attributes);
}
}
private function createAccessDeniedException(Request $request, array $attributes): AccessDeniedException
{
$exception = new AccessDeniedException();
$exception->setAttributes($attributes);
$exception->setSubject($request);
return $exception;
}
public static function getPriority(): int
{
return -255;
}
}
@@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Firewall;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Http\Authentication\AuthenticatorManagerInterface;
/**
* Firewall authentication listener that delegates to the authenticator system.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
class AuthenticatorManagerListener extends AbstractListener
{
private AuthenticatorManagerInterface $authenticatorManager;
public function __construct(AuthenticatorManagerInterface $authenticationManager)
{
$this->authenticatorManager = $authenticationManager;
}
public function supports(Request $request): ?bool
{
return $this->authenticatorManager->supports($request);
}
public function authenticate(RequestEvent $event): void
{
$request = $event->getRequest();
$response = $this->authenticatorManager->authenticateRequest($request);
if (null === $response) {
return;
}
$event->setResponse($response);
}
}
@@ -0,0 +1,100 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Firewall;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Http\AccessMapInterface;
/**
* ChannelListener switches the HTTP protocol based on the access control
* configuration.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @final
*/
class ChannelListener extends AbstractListener
{
private AccessMapInterface $map;
private ?LoggerInterface $logger;
private int $httpPort;
private int $httpsPort;
public function __construct(AccessMapInterface $map, ?LoggerInterface $logger = null, int $httpPort = 80, int $httpsPort = 443)
{
$this->map = $map;
$this->logger = $logger;
$this->httpPort = $httpPort;
$this->httpsPort = $httpsPort;
}
/**
* Handles channel management.
*/
public function supports(Request $request): ?bool
{
[, $channel] = $this->map->getPatterns($request);
if ('https' === $channel && !$request->isSecure()) {
if (null !== $this->logger) {
if ('https' === $request->headers->get('X-Forwarded-Proto')) {
$this->logger->info('Redirecting to HTTPS. ("X-Forwarded-Proto" header is set to "https" - did you set "trusted_proxies" correctly?)');
} elseif (str_contains($request->headers->get('Forwarded', ''), 'proto=https')) {
$this->logger->info('Redirecting to HTTPS. ("Forwarded" header is set to "proto=https" - did you set "trusted_proxies" correctly?)');
} else {
$this->logger->info('Redirecting to HTTPS.');
}
}
return true;
}
if ('http' === $channel && $request->isSecure()) {
$this->logger?->info('Redirecting to HTTP.');
return true;
}
return false;
}
public function authenticate(RequestEvent $event): void
{
$request = $event->getRequest();
$event->setResponse($this->createRedirectResponse($request));
}
private function createRedirectResponse(Request $request): RedirectResponse
{
$scheme = $request->isSecure() ? 'http' : 'https';
if ('http' === $scheme && 80 != $this->httpPort) {
$port = ':'.$this->httpPort;
} elseif ('https' === $scheme && 443 != $this->httpsPort) {
$port = ':'.$this->httpsPort;
} else {
$port = '';
}
$qs = $request->getQueryString();
if (null !== $qs) {
$qs = '?'.$qs;
}
$url = $scheme.'://'.$request->getHost().$port.$request->getBaseUrl().$request->getPathInfo().$qs;
return new RedirectResponse($url, 301);
}
}
@@ -0,0 +1,326 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Firewall;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver;
use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface;
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\EquatableInterface;
use Symfony\Component\Security\Core\User\LegacyPasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Event\TokenDeauthenticatedEvent;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* ContextListener manages the SecurityContext persistence through a session.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*
* @final
*/
class ContextListener extends AbstractListener
{
private TokenStorageInterface $tokenStorage;
private string $sessionKey;
private ?LoggerInterface $logger;
private iterable $userProviders;
private ?EventDispatcherInterface $dispatcher;
private bool $registered = false;
private AuthenticationTrustResolverInterface $trustResolver;
private ?\Closure $sessionTrackerEnabler;
/**
* @param iterable<mixed, UserProviderInterface> $userProviders
*/
public function __construct(TokenStorageInterface $tokenStorage, iterable $userProviders, string $contextKey, ?LoggerInterface $logger = null, ?EventDispatcherInterface $dispatcher = null, ?AuthenticationTrustResolverInterface $trustResolver = null, ?callable $sessionTrackerEnabler = null)
{
if (!$contextKey) {
throw new \InvalidArgumentException('$contextKey must not be empty.');
}
$this->tokenStorage = $tokenStorage;
$this->userProviders = $userProviders;
$this->sessionKey = '_security_'.$contextKey;
$this->logger = $logger;
$this->dispatcher = $dispatcher;
$this->trustResolver = $trustResolver ?? new AuthenticationTrustResolver();
$this->sessionTrackerEnabler = null === $sessionTrackerEnabler ? null : $sessionTrackerEnabler(...);
}
public function supports(Request $request): ?bool
{
return null; // always run authenticate() lazily with lazy firewalls
}
/**
* Reads the Security Token from the session.
*/
public function authenticate(RequestEvent $event): void
{
if (!$this->registered && null !== $this->dispatcher && $event->isMainRequest()) {
$this->dispatcher->addListener(KernelEvents::RESPONSE, $this->onKernelResponse(...));
$this->registered = true;
}
$request = $event->getRequest();
$session = $request->hasPreviousSession() ? $request->getSession() : null;
$request->attributes->set('_security_firewall_run', $this->sessionKey);
if (null !== $session) {
$usageIndexValue = $session instanceof Session ? $usageIndexReference = &$session->getUsageIndex() : 0;
$usageIndexReference = \PHP_INT_MIN;
$sessionId = $request->cookies->all()[$session->getName()] ?? null;
$token = $session->get($this->sessionKey);
// sessionId = true is used in the tests
if ($this->sessionTrackerEnabler && \in_array($sessionId, [true, $session->getId()], true)) {
$usageIndexReference = $usageIndexValue;
} else {
$usageIndexReference = $usageIndexReference - \PHP_INT_MIN + $usageIndexValue;
}
}
if (null === $session || null === $token) {
if ($this->sessionTrackerEnabler) {
($this->sessionTrackerEnabler)();
}
$this->tokenStorage->setToken(null);
return;
}
$token = $this->safelyUnserialize($token);
$this->logger?->debug('Read existing security token from the session.', [
'key' => $this->sessionKey,
'token_class' => \is_object($token) ? $token::class : null,
]);
if ($token instanceof TokenInterface) {
$originalToken = $token;
$token = $this->refreshUser($token);
if (!$token) {
$this->logger?->debug('Token was deauthenticated after trying to refresh it.');
$this->dispatcher?->dispatch(new TokenDeauthenticatedEvent($originalToken, $request));
}
} elseif (null !== $token) {
$this->logger?->warning('Expected a security token from the session, got something else.', ['key' => $this->sessionKey, 'received' => $token]);
$token = null;
}
if ($this->sessionTrackerEnabler) {
($this->sessionTrackerEnabler)();
}
$this->tokenStorage->setToken($token);
}
/**
* Writes the security token into the session.
*/
public function onKernelResponse(ResponseEvent $event): void
{
if (!$event->isMainRequest()) {
return;
}
$request = $event->getRequest();
if (!$request->hasSession() || $request->attributes->get('_security_firewall_run') !== $this->sessionKey) {
return;
}
$this->dispatcher?->removeListener(KernelEvents::RESPONSE, $this->onKernelResponse(...));
$this->registered = false;
$session = $request->getSession();
$sessionId = $session->getId();
$usageIndexValue = $session instanceof Session ? $usageIndexReference = &$session->getUsageIndex() : null;
$token = $this->tokenStorage->getToken();
if (!$this->trustResolver->isAuthenticated($token)) {
if ($request->hasPreviousSession()) {
$session->remove($this->sessionKey);
}
} else {
$session->set($this->sessionKey, serialize($token));
$this->logger?->debug('Stored the security token in the session.', ['key' => $this->sessionKey]);
}
if ($this->sessionTrackerEnabler && $session->getId() === $sessionId) {
$usageIndexReference = $usageIndexValue;
}
}
/**
* Refreshes the user by reloading it from the user provider.
*
* @throws \RuntimeException
*/
protected function refreshUser(TokenInterface $token): ?TokenInterface
{
$user = $token->getUser();
$userNotFoundByProvider = false;
$userDeauthenticated = false;
$userClass = $user::class;
foreach ($this->userProviders as $provider) {
if (!$provider instanceof UserProviderInterface) {
throw new \InvalidArgumentException(sprintf('User provider "%s" must implement "%s".', get_debug_type($provider), UserProviderInterface::class));
}
if (!$provider->supportsClass($userClass)) {
continue;
}
try {
$refreshedUser = $provider->refreshUser($user);
$newToken = clone $token;
$newToken->setUser($refreshedUser, false);
// tokens can be deauthenticated if the user has been changed.
if ($token instanceof AbstractToken && $this->hasUserChanged($user, $newToken)) {
$userDeauthenticated = true;
$this->logger?->debug('Cannot refresh token because user has changed.', ['username' => $refreshedUser->getUserIdentifier(), 'provider' => $provider::class]);
continue;
}
$token->setUser($refreshedUser);
if (null !== $this->logger) {
$context = ['provider' => $provider::class, 'username' => $refreshedUser->getUserIdentifier()];
if ($token instanceof SwitchUserToken) {
$originalToken = $token->getOriginalToken();
$context['impersonator_username'] = $originalToken->getUserIdentifier();
}
$this->logger->debug('User was reloaded from a user provider.', $context);
}
return $token;
} catch (UnsupportedUserException) {
// let's try the next user provider
} catch (UserNotFoundException $e) {
$this->logger?->info('Username could not be found in the selected user provider.', ['username' => $e->getUserIdentifier(), 'provider' => $provider::class]);
$userNotFoundByProvider = true;
}
}
if ($userDeauthenticated) {
return null;
}
if ($userNotFoundByProvider) {
return null;
}
throw new \RuntimeException(sprintf('There is no user provider for user "%s". Shouldn\'t the "supportsClass()" method of your user provider return true for this classname?', $userClass));
}
private function safelyUnserialize(string $serializedToken): mixed
{
$token = null;
$prevUnserializeHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback');
$prevErrorHandler = set_error_handler(function ($type, $msg, $file, $line, $context = []) use (&$prevErrorHandler) {
if (__FILE__ === $file && !\in_array($type, [\E_DEPRECATED, \E_USER_DEPRECATED], true)) {
throw new \ErrorException($msg, 0x37313BC, $type, $file, $line);
}
return $prevErrorHandler ? $prevErrorHandler($type, $msg, $file, $line, $context) : false;
});
try {
$token = unserialize($serializedToken);
} catch (\ErrorException $e) {
if (0x37313BC !== $e->getCode()) {
throw $e;
}
$this->logger?->warning('Failed to unserialize the security token from the session.', ['key' => $this->sessionKey, 'received' => $serializedToken, 'exception' => $e]);
} finally {
restore_error_handler();
ini_set('unserialize_callback_func', $prevUnserializeHandler);
}
return $token;
}
private static function hasUserChanged(UserInterface $originalUser, TokenInterface $refreshedToken): bool
{
$refreshedUser = $refreshedToken->getUser();
if ($originalUser instanceof EquatableInterface) {
return !$originalUser->isEqualTo($refreshedUser);
}
if ($originalUser instanceof PasswordAuthenticatedUserInterface || $refreshedUser instanceof PasswordAuthenticatedUserInterface) {
if (!$originalUser instanceof PasswordAuthenticatedUserInterface || !$refreshedUser instanceof PasswordAuthenticatedUserInterface || $originalUser->getPassword() !== $refreshedUser->getPassword()) {
return true;
}
if ($originalUser instanceof LegacyPasswordAuthenticatedUserInterface xor $refreshedUser instanceof LegacyPasswordAuthenticatedUserInterface) {
return true;
}
if ($originalUser instanceof LegacyPasswordAuthenticatedUserInterface && $refreshedUser instanceof LegacyPasswordAuthenticatedUserInterface && $originalUser->getSalt() !== $refreshedUser->getSalt()) {
return true;
}
}
$userRoles = array_map('strval', (array) $refreshedUser->getRoles());
if (
\count($userRoles) !== \count($refreshedToken->getRoleNames())
|| \count($userRoles) !== \count(array_intersect($userRoles, $refreshedToken->getRoleNames()))
) {
return true;
}
if ($originalUser->getUserIdentifier() !== $refreshedUser->getUserIdentifier()) {
return true;
}
return false;
}
/**
* @internal
*/
public static function handleUnserializeCallback(string $class): never
{
throw new \ErrorException('Class not found: '.$class, 0x37313BC);
}
}
@@ -0,0 +1,234 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Firewall;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Exception\AccountStatusException;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\InsufficientAuthenticationException;
use Symfony\Component\Security\Core\Exception\LazyResponseException;
use Symfony\Component\Security\Core\Exception\LogoutException;
use Symfony\Component\Security\Http\Authorization\AccessDeniedHandlerInterface;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
use Symfony\Component\Security\Http\EntryPoint\Exception\NotAnEntryPointException;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Component\Security\Http\SecurityRequestAttributes;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
/**
* ExceptionListener catches authentication exception and converts them to
* Response instances.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @final
*/
class ExceptionListener
{
use TargetPathTrait;
private TokenStorageInterface $tokenStorage;
private string $firewallName;
private ?AccessDeniedHandlerInterface $accessDeniedHandler;
private ?AuthenticationEntryPointInterface $authenticationEntryPoint;
private AuthenticationTrustResolverInterface $authenticationTrustResolver;
private ?string $errorPage;
private ?LoggerInterface $logger;
private HttpUtils $httpUtils;
private bool $stateless;
public function __construct(TokenStorageInterface $tokenStorage, AuthenticationTrustResolverInterface $trustResolver, HttpUtils $httpUtils, string $firewallName, ?AuthenticationEntryPointInterface $authenticationEntryPoint = null, ?string $errorPage = null, ?AccessDeniedHandlerInterface $accessDeniedHandler = null, ?LoggerInterface $logger = null, bool $stateless = false)
{
$this->tokenStorage = $tokenStorage;
$this->accessDeniedHandler = $accessDeniedHandler;
$this->httpUtils = $httpUtils;
$this->firewallName = $firewallName;
$this->authenticationEntryPoint = $authenticationEntryPoint;
$this->authenticationTrustResolver = $trustResolver;
$this->errorPage = $errorPage;
$this->logger = $logger;
$this->stateless = $stateless;
}
/**
* Registers a onKernelException listener to take care of security exceptions.
*/
public function register(EventDispatcherInterface $dispatcher): void
{
$dispatcher->addListener(KernelEvents::EXCEPTION, $this->onKernelException(...), 1);
}
/**
* Unregisters the dispatcher.
*/
public function unregister(EventDispatcherInterface $dispatcher): void
{
$dispatcher->removeListener(KernelEvents::EXCEPTION, $this->onKernelException(...));
}
/**
* Handles security related exceptions.
*/
public function onKernelException(ExceptionEvent $event): void
{
$exception = $event->getThrowable();
do {
if ($exception instanceof AuthenticationException) {
$this->handleAuthenticationException($event, $exception);
return;
}
if ($exception instanceof AccessDeniedException) {
$this->handleAccessDeniedException($event, $exception);
return;
}
if ($exception instanceof LazyResponseException) {
$event->setResponse($exception->getResponse());
return;
}
if ($exception instanceof LogoutException) {
$this->handleLogoutException($event, $exception);
return;
}
} while (null !== $exception = $exception->getPrevious());
}
private function handleAuthenticationException(ExceptionEvent $event, AuthenticationException $exception): void
{
$this->logger?->info('An AuthenticationException was thrown; redirecting to authentication entry point.', ['exception' => $exception]);
try {
$event->setResponse($this->startAuthentication($event->getRequest(), $exception));
$event->allowCustomResponseCode();
} catch (\Exception $e) {
$event->setThrowable($e);
}
}
private function handleAccessDeniedException(ExceptionEvent $event, AccessDeniedException $exception): void
{
$event->setThrowable(new AccessDeniedHttpException($exception->getMessage(), $exception));
$token = $this->tokenStorage->getToken();
if (!$this->authenticationTrustResolver->isFullFledged($token)) {
$this->logger?->debug('Access denied, the user is not fully authenticated; redirecting to authentication entry point.', ['exception' => $exception]);
try {
$insufficientAuthenticationException = new InsufficientAuthenticationException('Full authentication is required to access this resource.', 0, $exception);
if (null !== $token) {
$insufficientAuthenticationException->setToken($token);
}
$event->setResponse($this->startAuthentication($event->getRequest(), $insufficientAuthenticationException));
} catch (\Exception $e) {
$event->setThrowable($e);
}
return;
}
$this->logger?->debug('Access denied, the user is neither anonymous, nor remember-me.', ['exception' => $exception]);
try {
if (null !== $this->accessDeniedHandler) {
$response = $this->accessDeniedHandler->handle($event->getRequest(), $exception);
if ($response instanceof Response) {
$event->setResponse($response);
}
} elseif (null !== $this->errorPage) {
$subRequest = $this->httpUtils->createRequest($event->getRequest(), $this->errorPage);
$subRequest->attributes->set(SecurityRequestAttributes::ACCESS_DENIED_ERROR, $exception);
$event->setResponse($event->getKernel()->handle($subRequest, HttpKernelInterface::SUB_REQUEST, true));
$event->allowCustomResponseCode();
}
} catch (\Exception $e) {
$this->logger?->error('An exception was thrown when handling an AccessDeniedException.', ['exception' => $e]);
$event->setThrowable(new \RuntimeException('Exception thrown when handling an exception.', 0, $e));
}
}
private function handleLogoutException(ExceptionEvent $event, LogoutException $exception): void
{
$event->setThrowable(new AccessDeniedHttpException($exception->getMessage(), $exception));
$this->logger?->info('A LogoutException was thrown; wrapping with AccessDeniedHttpException', ['exception' => $exception]);
}
private function startAuthentication(Request $request, AuthenticationException $authException): Response
{
if (null === $this->authenticationEntryPoint) {
$this->throwUnauthorizedException($authException);
}
$this->logger?->debug('Calling Authentication entry point.', ['entry_point' => $this->authenticationEntryPoint]);
if (!$this->stateless) {
$this->setTargetPath($request);
}
if ($authException instanceof AccountStatusException) {
// remove the security token to prevent infinite redirect loops
$this->tokenStorage->setToken(null);
$this->logger?->info('The security token was removed due to an AccountStatusException.', ['exception' => $authException]);
}
try {
$response = $this->authenticationEntryPoint->start($request, $authException);
} catch (NotAnEntryPointException) {
$this->throwUnauthorizedException($authException);
}
if (!$response instanceof Response) {
$given = get_debug_type($response);
throw new \LogicException(sprintf('The "%s::start()" method must return a Response object ("%s" returned).', get_debug_type($this->authenticationEntryPoint), $given));
}
return $response;
}
protected function setTargetPath(Request $request): void
{
// session isn't required when using HTTP basic authentication mechanism for example
if ($request->hasSession() && $request->isMethodSafe() && !$request->isXmlHttpRequest()) {
$this->saveTargetPath($request->getSession(), $this->firewallName, $request->getUri());
}
}
private function throwUnauthorizedException(AuthenticationException $authException): never
{
$this->logger?->notice(sprintf('No Authentication entry point configured, returning a %s HTTP response. Configure "entry_point" on the firewall "%s" if you want to modify the response.', Response::HTTP_UNAUTHORIZED, $this->firewallName));
throw new HttpException(Response::HTTP_UNAUTHORIZED, $authException->getMessage(), $authException, [], $authException->getCode());
}
}
@@ -0,0 +1,43 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Firewall;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
/**
* Can be implemented by firewall listeners.
*
* @author Christian Scheb <me@christianscheb.de>
* @author Nicolas Grekas <p@tchwork.com>
* @author Robin Chalas <robin.chalas@gmail.com>
*/
interface FirewallListenerInterface
{
/**
* Tells whether the authenticate() method should be called or not depending on the incoming request.
*
* Returning null means authenticate() can be called lazily when accessing the token storage.
*/
public function supports(Request $request): ?bool;
/**
* Does whatever is required to authenticate the request, typically calling $event->setResponse() internally.
*/
public function authenticate(RequestEvent $event): void;
/**
* Defines the priority of the listener.
* The higher the number, the earlier a listener is executed.
*/
public static function getPriority(): int;
}
+111
View File
@@ -0,0 +1,111 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Firewall;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Exception\LogoutException;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Http\Event\LogoutEvent;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Component\Security\Http\ParameterBagUtils;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* LogoutListener logout users.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @final
*/
class LogoutListener extends AbstractListener
{
private TokenStorageInterface $tokenStorage;
private array $options;
private HttpUtils $httpUtils;
private ?CsrfTokenManagerInterface $csrfTokenManager;
private EventDispatcherInterface $eventDispatcher;
/**
* @param array $options An array of options to process a logout attempt
*/
public function __construct(TokenStorageInterface $tokenStorage, HttpUtils $httpUtils, EventDispatcherInterface $eventDispatcher, array $options = [], ?CsrfTokenManagerInterface $csrfTokenManager = null)
{
$this->tokenStorage = $tokenStorage;
$this->httpUtils = $httpUtils;
$this->options = array_merge([
'csrf_parameter' => '_csrf_token',
'csrf_token_id' => 'logout',
'logout_path' => '/logout',
], $options);
$this->csrfTokenManager = $csrfTokenManager;
$this->eventDispatcher = $eventDispatcher;
}
public function supports(Request $request): ?bool
{
return $this->requiresLogout($request);
}
/**
* Performs the logout if requested.
*
* If a CsrfTokenManagerInterface instance is available, it will be used to
* validate the request.
*
* @throws LogoutException if the CSRF token is invalid
* @throws \RuntimeException if the LogoutEvent listener does not set a response
*/
public function authenticate(RequestEvent $event): void
{
$request = $event->getRequest();
if (null !== $this->csrfTokenManager) {
$csrfToken = ParameterBagUtils::getRequestParameterValue($request, $this->options['csrf_parameter']);
if (!\is_string($csrfToken) || false === $this->csrfTokenManager->isTokenValid(new CsrfToken($this->options['csrf_token_id'], $csrfToken))) {
throw new LogoutException('Invalid CSRF token.');
}
}
$logoutEvent = new LogoutEvent($request, $this->tokenStorage->getToken());
$this->eventDispatcher->dispatch($logoutEvent);
if (!$response = $logoutEvent->getResponse()) {
throw new \RuntimeException('No logout listener set the Response, make sure at least the DefaultLogoutListener is registered.');
}
$this->tokenStorage->setToken(null);
$event->setResponse($response);
}
/**
* Whether this request is asking for logout.
*
* The default implementation only processed requests to a specific path,
* but a subclass could change this to logout requests where
* certain parameters is present.
*/
protected function requiresLogout(Request $request): bool
{
return isset($this->options['logout_path']) && $this->httpUtils->checkRequestPath($request, $this->options['logout_path']);
}
public static function getPriority(): int
{
return -127;
}
}
@@ -0,0 +1,226 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Firewall;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Event\SwitchUserEvent;
use Symfony\Component\Security\Http\SecurityEvents;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* SwitchUserListener allows a user to impersonate another one temporarily
* (like the Unix su command).
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @final
*/
class SwitchUserListener extends AbstractListener
{
public const EXIT_VALUE = '_exit';
private TokenStorageInterface $tokenStorage;
private UserProviderInterface $provider;
private UserCheckerInterface $userChecker;
private string $firewallName;
private AccessDecisionManagerInterface $accessDecisionManager;
private string $usernameParameter;
private string $role;
private ?LoggerInterface $logger;
private ?EventDispatcherInterface $dispatcher;
private bool $stateless;
private ?UrlGeneratorInterface $urlGenerator;
private ?string $targetRoute;
public function __construct(TokenStorageInterface $tokenStorage, UserProviderInterface $provider, UserCheckerInterface $userChecker, string $firewallName, AccessDecisionManagerInterface $accessDecisionManager, ?LoggerInterface $logger = null, string $usernameParameter = '_switch_user', string $role = 'ROLE_ALLOWED_TO_SWITCH', ?EventDispatcherInterface $dispatcher = null, bool $stateless = false, ?UrlGeneratorInterface $urlGenerator = null, ?string $targetRoute = null)
{
if ('' === $firewallName) {
throw new \InvalidArgumentException('$firewallName must not be empty.');
}
$this->tokenStorage = $tokenStorage;
$this->provider = $provider;
$this->userChecker = $userChecker;
$this->firewallName = $firewallName;
$this->accessDecisionManager = $accessDecisionManager;
$this->usernameParameter = $usernameParameter;
$this->role = $role;
$this->logger = $logger;
$this->dispatcher = $dispatcher;
$this->stateless = $stateless;
$this->urlGenerator = $urlGenerator;
$this->targetRoute = $targetRoute;
}
public function supports(Request $request): ?bool
{
// usernames can be falsy
$username = $request->get($this->usernameParameter);
if (null === $username || '' === $username) {
$username = $request->headers->get($this->usernameParameter);
}
// if it's still "empty", nothing to do.
if (null === $username || '' === $username) {
return false;
}
$request->attributes->set('_switch_user_username', $username);
return true;
}
/**
* Handles the switch to another user.
*
* @throws \LogicException if switching to a user failed
*/
public function authenticate(RequestEvent $event): void
{
$request = $event->getRequest();
$username = $request->attributes->get('_switch_user_username');
$request->attributes->remove('_switch_user_username');
if (null === $this->tokenStorage->getToken()) {
throw new AuthenticationCredentialsNotFoundException('Could not find original Token object.');
}
if (self::EXIT_VALUE === $username) {
$this->tokenStorage->setToken($this->attemptExitUser($request));
} else {
try {
$this->tokenStorage->setToken($this->attemptSwitchUser($request, $username));
} catch (AuthenticationException $e) {
// Generate 403 in any conditions to prevent user enumeration vulnerabilities
throw new AccessDeniedException('Switch User failed: '.$e->getMessage(), $e);
}
}
if (!$this->stateless) {
$request->query->remove($this->usernameParameter);
$request->server->set('QUERY_STRING', http_build_query($request->query->all(), '', '&'));
$response = new RedirectResponse($this->urlGenerator && $this->targetRoute ? $this->urlGenerator->generate($this->targetRoute) : $request->getUri(), 302);
$event->setResponse($response);
}
}
/**
* Attempts to switch to another user and returns the new token if successfully switched.
*
* @throws \LogicException
* @throws AccessDeniedException
*/
private function attemptSwitchUser(Request $request, string $username): ?TokenInterface
{
$token = $this->tokenStorage->getToken();
$originalToken = $this->getOriginalToken($token);
if (null !== $originalToken) {
if ($token->getUserIdentifier() === $username) {
return $token;
}
// User already switched, exit before seamlessly switching to another user
$token = $this->attemptExitUser($request);
}
$currentUsername = $token->getUserIdentifier();
$nonExistentUsername = '_'.hash('xxh128', random_bytes(8).$username);
// To protect against user enumeration via timing measurements
// we always load both successfully and unsuccessfully
try {
$user = $this->provider->loadUserByIdentifier($username);
try {
$this->provider->loadUserByIdentifier($nonExistentUsername);
} catch (\Exception) {
}
} catch (AuthenticationException $e) {
$this->provider->loadUserByIdentifier($currentUsername);
throw $e;
}
if (false === $this->accessDecisionManager->decide($token, [$this->role], $user)) {
$exception = new AccessDeniedException();
$exception->setAttributes($this->role);
throw $exception;
}
$this->logger?->info('Attempting to switch to user.', ['username' => $username]);
$this->userChecker->checkPostAuth($user);
$roles = $user->getRoles();
$originatedFromUri = str_replace('/&', '/?', preg_replace('#[&?]'.$this->usernameParameter.'=[^&]*#', '', $request->getRequestUri()));
$token = new SwitchUserToken($user, $this->firewallName, $roles, $token, $originatedFromUri);
if (null !== $this->dispatcher) {
$switchEvent = new SwitchUserEvent($request, $token->getUser(), $token);
$this->dispatcher->dispatch($switchEvent, SecurityEvents::SWITCH_USER);
// use the token from the event in case any listeners have replaced it.
$token = $switchEvent->getToken();
}
return $token;
}
/**
* Attempts to exit from an already switched user and returns the original token.
*
* @throws AuthenticationCredentialsNotFoundException
*/
private function attemptExitUser(Request $request): TokenInterface
{
if (null === ($currentToken = $this->tokenStorage->getToken()) || null === $original = $this->getOriginalToken($currentToken)) {
throw new AuthenticationCredentialsNotFoundException('Could not find original Token object.');
}
if (null !== $this->dispatcher && $original->getUser() instanceof UserInterface) {
$user = $this->provider->refreshUser($original->getUser());
$original->setUser($user);
$switchEvent = new SwitchUserEvent($request, $user, $original);
$this->dispatcher->dispatch($switchEvent, SecurityEvents::SWITCH_USER);
$original = $switchEvent->getToken();
}
return $original;
}
private function getOriginalToken(TokenInterface $token): ?TokenInterface
{
if ($token instanceof SwitchUserToken) {
return $token->getOriginalToken();
}
return null;
}
}
+50
View File
@@ -0,0 +1,50 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
use Symfony\Component\Security\Http\Firewall\ExceptionListener;
use Symfony\Component\Security\Http\Firewall\LogoutListener;
/**
* FirewallMap allows configuration of different firewalls for specific parts
* of the website.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class FirewallMap implements FirewallMapInterface
{
/**
* @var list<array{RequestMatcherInterface, list<callable>, ExceptionListener|null, LogoutListener|null}>
*/
private array $map = [];
/**
* @param list<callable> $listeners
*/
public function add(?RequestMatcherInterface $requestMatcher = null, array $listeners = [], ?ExceptionListener $exceptionListener = null, ?LogoutListener $logoutListener = null): void
{
$this->map[] = [$requestMatcher, $listeners, $exceptionListener, $logoutListener];
}
public function getListeners(Request $request): array
{
foreach ($this->map as $elements) {
if (null === $elements[0] || $elements[0]->matches($request)) {
return [$elements[1], $elements[2], $elements[3]];
}
}
return [[], null, null];
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\Firewall\ExceptionListener;
use Symfony\Component\Security\Http\Firewall\LogoutListener;
/**
* This interface must be implemented by firewall maps.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
interface FirewallMapInterface
{
/**
* Returns the authentication listeners, and the exception listener to use
* for the given request.
*
* If there are no authentication listeners, the first inner array must be
* empty.
*
* If there is no exception listener, the second element of the outer array
* must be null.
*
* If there is no logout listener, the third element of the outer array
* must be null.
*
* @return array{iterable<mixed, callable>, ExceptionListener, LogoutListener}
*/
public function getListeners(Request $request): array;
}
+172
View File
@@ -0,0 +1,172 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
/**
* Encapsulates the logic needed to create sub-requests, redirect the user, and match URLs.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class HttpUtils
{
private ?UrlGeneratorInterface $urlGenerator;
private UrlMatcherInterface|RequestMatcherInterface|null $urlMatcher;
private ?string $domainRegexp;
private ?string $secureDomainRegexp;
/**
* @param $domainRegexp A regexp the target of HTTP redirections must match, scheme included
* @param $secureDomainRegexp A regexp the target of HTTP redirections must match when the scheme is "https"
*
* @throws \InvalidArgumentException
*/
public function __construct(?UrlGeneratorInterface $urlGenerator = null, UrlMatcherInterface|RequestMatcherInterface|null $urlMatcher = null, ?string $domainRegexp = null, ?string $secureDomainRegexp = null)
{
$this->urlGenerator = $urlGenerator;
$this->urlMatcher = $urlMatcher;
$this->domainRegexp = $domainRegexp;
$this->secureDomainRegexp = $secureDomainRegexp;
}
/**
* Creates a redirect Response.
*
* @param string $path A path (an absolute path (/foo), an absolute URL (http://...), or a route name (foo))
* @param int $status The HTTP status code (302 "Found" by default)
*/
public function createRedirectResponse(Request $request, string $path, int $status = 302): RedirectResponse
{
if (null !== $this->secureDomainRegexp && 'https' === $this->urlMatcher->getContext()->getScheme() && preg_match('#^https?:[/\\\\]{2,}+[^/]++#i', $path, $host) && !preg_match(sprintf($this->secureDomainRegexp, preg_quote($request->getHttpHost())), $host[0])) {
$path = '/';
}
if (null !== $this->domainRegexp && preg_match('#^https?:[/\\\\]{2,}+[^/]++#i', $path, $host) && !preg_match(sprintf($this->domainRegexp, preg_quote($request->getHttpHost())), $host[0])) {
$path = '/';
}
return new RedirectResponse($this->generateUri($request, $path), $status);
}
/**
* Creates a Request.
*
* @param string $path A path (an absolute path (/foo), an absolute URL (http://...), or a route name (foo))
*/
public function createRequest(Request $request, string $path): Request
{
$newRequest = Request::create($this->generateUri($request, $path), 'get', [], $request->cookies->all(), [], $request->server->all());
static $setSession;
$setSession ??= \Closure::bind(static function ($newRequest, $request) { $newRequest->session = $request->session; }, null, Request::class);
$setSession($newRequest, $request);
if ($request->attributes->has(SecurityRequestAttributes::AUTHENTICATION_ERROR)) {
$newRequest->attributes->set(SecurityRequestAttributes::AUTHENTICATION_ERROR, $request->attributes->get(SecurityRequestAttributes::AUTHENTICATION_ERROR));
}
if ($request->attributes->has(SecurityRequestAttributes::ACCESS_DENIED_ERROR)) {
$newRequest->attributes->set(SecurityRequestAttributes::ACCESS_DENIED_ERROR, $request->attributes->get(SecurityRequestAttributes::ACCESS_DENIED_ERROR));
}
if ($request->attributes->has(SecurityRequestAttributes::LAST_USERNAME)) {
$newRequest->attributes->set(SecurityRequestAttributes::LAST_USERNAME, $request->attributes->get(SecurityRequestAttributes::LAST_USERNAME));
}
if ($request->get('_format')) {
$newRequest->attributes->set('_format', $request->get('_format'));
}
if ($request->getDefaultLocale() !== $request->getLocale()) {
$newRequest->setLocale($request->getLocale());
}
return $newRequest;
}
/**
* Checks that a given path matches the Request.
*
* @param string $path A path (an absolute path (/foo), an absolute URL (http://...), or a route name (foo))
*
* @return bool true if the path is the same as the one from the Request, false otherwise
*/
public function checkRequestPath(Request $request, string $path): bool
{
if ('/' !== $path[0]) {
// Shortcut if request has already been matched before
if ($request->attributes->has('_route')) {
return $path === $request->attributes->get('_route');
}
try {
// matching a request is more powerful than matching a URL path + context, so try that first
if ($this->urlMatcher instanceof RequestMatcherInterface) {
$parameters = $this->urlMatcher->matchRequest($request);
} else {
$parameters = $this->urlMatcher->match($request->getPathInfo());
}
return isset($parameters['_route']) && $path === $parameters['_route'];
} catch (MethodNotAllowedException|ResourceNotFoundException) {
return false;
}
}
return $path === rawurldecode($request->getPathInfo());
}
/**
* Generates a URI, based on the given path or absolute URL.
*
* @param string $path A path (an absolute path (/foo), an absolute URL (http://...), or a route name (foo))
*
* @throws \LogicException
*/
public function generateUri(Request $request, string $path): string
{
$url = parse_url($path);
if ('' === $path || isset($url['scheme'], $url['host'])) {
return $path;
}
if ('/' === $path[0]) {
return $request->getUriForPath($path);
}
if (null === $this->urlGenerator) {
throw new \LogicException('You must provide a UrlGeneratorInterface instance to be able to use routes.');
}
$url = $this->urlGenerator->generate($path, $request->attributes->all(), UrlGeneratorInterface::ABSOLUTE_URL);
// unnecessary query string parameters must be removed from URL
// (ie. query parameters that are presents in $attributes)
// fortunately, they all are, so we have to remove entire query string
$position = strpos($url, '?');
if (false !== $position) {
$fragment = parse_url($url, \PHP_URL_FRAGMENT);
$url = substr($url, 0, $position);
// fragment must be preserved
if ($fragment) {
$url .= "#$fragment";
}
}
return $url;
}
}
@@ -0,0 +1,92 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Impersonate;
use Symfony\Bundle\SecurityBundle\Security\FirewallMap;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
use Symfony\Component\Security\Http\Firewall\SwitchUserListener;
/**
* Provides generator functions for the impersonation urls.
*
* @author Amrouche Hamza <hamza.simperfit@gmail.com>
* @author Damien Fayet <damienf1521@gmail.com>
*/
class ImpersonateUrlGenerator
{
private RequestStack $requestStack;
private TokenStorageInterface $tokenStorage;
private FirewallMap $firewallMap;
public function __construct(RequestStack $requestStack, FirewallMap $firewallMap, TokenStorageInterface $tokenStorage)
{
$this->requestStack = $requestStack;
$this->tokenStorage = $tokenStorage;
$this->firewallMap = $firewallMap;
}
public function generateImpersonationPath(string $identifier): string
{
return $this->buildPath(null, $identifier);
}
public function generateImpersonationUrl(string $identifier): string
{
if (null === $request = $this->requestStack->getCurrentRequest()) {
return '';
}
return $request->getUriForPath($this->buildPath(null, $identifier));
}
public function generateExitPath(?string $targetUri = null): string
{
return $this->buildPath($targetUri);
}
public function generateExitUrl(?string $targetUri = null): string
{
if (null === $request = $this->requestStack->getCurrentRequest()) {
return '';
}
return $request->getUriForPath($this->buildPath($targetUri));
}
private function isImpersonatedUser(): bool
{
return $this->tokenStorage->getToken() instanceof SwitchUserToken;
}
private function buildPath(?string $targetUri = null, string $identifier = SwitchUserListener::EXIT_VALUE): string
{
if (null === ($request = $this->requestStack->getCurrentRequest())) {
return '';
}
if (!$this->isImpersonatedUser() && SwitchUserListener::EXIT_VALUE == $identifier) {
return '';
}
if (null === $switchUserConfig = $this->firewallMap->getFirewallConfig($request)->getSwitchUser()) {
throw new \LogicException('Unable to generate the impersonate URLs without a firewall configured for the user switch.');
}
$targetUri ??= $request->getRequestUri();
$targetUri .= (parse_url($targetUri, \PHP_URL_QUERY) ? '&' : '?').http_build_query([$switchUserConfig['parameter'] => $identifier], '', '&');
return $targetUri;
}
}

Some files were not shown because too many files have changed in this diff Show More