Subida del módulo y tema de PrestaShop

This commit is contained in:
Kaloyan
2026-04-09 18:31:51 +02:00
parent 12c253296f
commit 16b3ff9424
39262 changed files with 7418797 additions and 0 deletions

View File

@@ -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;
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)
*
* @return void
*/
public function add(RequestMatcherInterface $requestMatcher, array $attributes = [], ?string $channel = null)
{
$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];
}
}

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;
}

View File

@@ -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;
}

View File

@@ -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;
}

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\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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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.';
}
}

View File

@@ -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.';
}
}

View File

@@ -0,0 +1,109 @@
<?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\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 $signatureAlgorithm,
private JWK $jwk,
private string $audience,
private array $issuers,
private string $claim = 'sub',
private ?LoggerInterface $logger = null,
private ClockInterface $clock = new Clock(),
) {
}
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(new AlgorithmManager([$this->signatureAlgorithm]));
$serializerManager = new JWSSerializerManager([new CompactSerializer()]);
$jws = $serializerManager->unserialize($accessToken);
$claims = json_decode($jws->getPayload(), true);
// Verify the signature
if (!$jwsVerifier->verifyWithKey($jws, $this->jwk, 0)) {
throw new InvalidSignatureException();
}
// Verify the headers
$headerCheckerManager = new Checker\HeaderCheckerManager([
new Checker\AlgorithmChecker([$this->signatureAlgorithm->name()]),
], [
new JWSTokenSupport(),
]);
// if this check fails, an InvalidHeaderException is thrown
$headerCheckerManager->check($jws, 0);
// Verify the claims
$checkers = [
new Checker\IssuedAtChecker(0, false, $this->clock),
new Checker\NotBeforeChecker(0, false, $this->clock),
new Checker\ExpirationTimeChecker(0, false, $this->clock),
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(function () use ($claims) {
$claims['user_identifier'] = $claims[$this->claim];
return $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);
}
}
}

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\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::class)) {
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);
}
}

View File

@@ -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\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(function () use ($claims) {
$claims['user_identifier'] = $claims[$this->claim];
return $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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,27 @@
<?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
{
public function __construct(bool $disabled = false, string $resolver = UserValueResolver::class)
{
parent::__construct($resolver, $disabled);
}
}

View File

@@ -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\Attribute;
use Symfony\Component\ExpressionLanguage\Expression;
/**
* @author Ryan Weaver <ryan@knpuniversity.com>
*/
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION)]
final class IsGranted
{
public function __construct(
/**
* Sets the first argument that will be passed to isGranted().
*/
public string|Expression $attribute,
/**
* Sets the second argument passed to isGranted().
*
* @var array<string|Expression>|string|Expression|null
*/
public array|string|Expression|null $subject = null,
/**
* The message of the exception - has a nice default if not set.
*/
public ?string $message = null,
/**
* If set, will throw HttpKernel's HttpException with the given $statusCode.
* If null, Security\Core's AccessDeniedException will be used.
*/
public ?int $statusCode = null,
/**
* If set, will add the exception code to thrown exception.
*/
public ?int $exceptionCode = null,
) {
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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\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 $httpKernel;
protected $httpUtils;
protected $logger;
protected $options;
protected $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;
}
/**
* @return void
*/
public function setOptions(array $options)
{
$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']);
}
}

View File

@@ -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\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;
protected $logger;
protected $options;
protected $firewallName;
protected $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;
}
/**
* @return void
*/
public function setOptions(array $options)
{
$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'];
}
}

View File

@@ -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;
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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]);
}
}
}

View File

@@ -0,0 +1,126 @@
<?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)]
);
}
/**
* @return void
*/
public function setTranslator(?TranslatorInterface $translator)
{
$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));
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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 = [];
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,164 @@
<?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\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']);
$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 (!\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;
}
}

View File

@@ -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);
}
}

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\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;
}

View File

@@ -0,0 +1,175 @@
<?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'])) {
throw new BadRequestHttpException(\sprintf('The key "%s" must be a 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'])) {
throw new BadRequestHttpException(\sprintf('The key "%s" must be a string.', $this->options['password_path']));
}
} catch (AccessException $e) {
throw new BadRequestHttpException(\sprintf('The key "%s" must be provided.', $this->options['password_path']), $e);
}
if ('' === $credentials['username'] || '' === $credentials['password']) {
trigger_deprecation('symfony/security', '6.2', 'Passing an empty string as username or password parameter is deprecated.');
}
return $credentials;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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
}
}

View File

@@ -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;
}
}

View File

@@ -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
{
}

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\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;
}
}

View File

@@ -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;
}
}

View File

@@ -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\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 $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;
if (2 === \func_num_args()) {
$badgeFqcn = func_get_arg(1);
if (!\is_string($badgeFqcn)) {
throw new \LogicException(\sprintf('Second argument of "%s" must be a string.', __METHOD__));
}
}
$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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

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\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>
*
* @final
*
* @internal in Symfony 5.1
*/
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) ?: null;
}
}

View File

@@ -0,0 +1,68 @@
<?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;
// required for compatibility with Symfony 5.4
if (method_exists($this, 'setAuthenticated')) {
// this token is meant to be used after authentication success, so it is always authenticated
$this->setAuthenticated(true, false);
}
}
/**
* 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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

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\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.');
}
}

View File

@@ -0,0 +1,80 @@
<?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;
}
/**
* @deprecated since Symfony 6.2, use resolve() instead
*/
public function supports(Request $request, ArgumentMetadata $argument): bool
{
@trigger_deprecation('symfony/http-kernel', '6.2', 'The "%s()" method is deprecated, use "resolve()" instead.', __METHOD__);
// 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 false;
}
return true;
}
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()));
}
}

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\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);
*
* @return Response
*/
public function start(Request $request, ?AuthenticationException $authException = null);
}

View File

@@ -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
{
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

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\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;
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,86 @@
<?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;
}
/**
* @return void
*/
public function setResponse(?Response $response)
{
$this->response = $response;
}
public function getResponse(): ?Response
{
return $this->response;
}
public function getPassport(): ?Passport
{
return $this->passport;
}
}

View File

@@ -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;
}
}

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;
}
}

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;
}
}

View File

@@ -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;
}
}

View File

@@ -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'];
}
}

View File

@@ -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]];
}
}

View File

@@ -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',
];
}
}

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\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],
];
}
}

View File

@@ -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]];
}
}

View File

@@ -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',
];
}
}

View File

@@ -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],
];
}
}

View File

@@ -0,0 +1,122 @@
<?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,
) {
}
/**
* @return void
*/
public function onKernelControllerArguments(ControllerArgumentsEvent $event)
{
/** @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;
}
}

View File

@@ -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',
];
}
}

View File

@@ -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'];
}
}

View File

@@ -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',
];
}
}

View File

@@ -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',
];
}
}

View File

@@ -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'];
}
}

View File

@@ -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],
];
}
}

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\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(...));
}
}

View File

@@ -0,0 +1,144 @@
<?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) {
if (!$listener instanceof FirewallListenerInterface) {
$listener($event);
} elseif (false !== $listener->supports($event->getRequest())) {
$listener->authenticate($event);
}
if ($event->hasResponse()) {
break;
}
}
}
private function getListenerPriority(object $logoutListener): int
{
return $logoutListener instanceof FirewallListenerInterface ? $logoutListener->getPriority() : 0;
}
}

View File

@@ -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
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,337 @@
<?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 (empty($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) {
if (!$token->getUser()) {
throw new \UnexpectedValueException(\sprintf('Cannot authenticate a "%s" token because it doesn\'t store a user.', $token::class));
}
$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;
$usageIndexReference = \PHP_INT_MIN;
$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;
} else {
$usageIndexReference = $usageIndexReference - \PHP_INT_MIN + $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?->warning('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 ($refreshedToken instanceof SwitchUserToken) {
$userRoles[] = 'ROLE_PREVIOUS_ADMIN';
}
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);
}
}

View File

@@ -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());
}
}

View File

@@ -0,0 +1,45 @@
<?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.
*
* @return void
*/
public function authenticate(RequestEvent $event);
/**
* Defines the priority of the listener.
* The higher the number, the earlier a listener is executed.
*/
public static function getPriority(): int;
}

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;
}
}

View File

@@ -0,0 +1,229 @@
<?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->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 && self::EXIT_VALUE !== $username ? $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();
$roles[] = 'ROLE_PREVIOUS_ADMIN';
$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();
}
$this->tokenStorage->setToken($original);
return $original;
}
private function getOriginalToken(TokenInterface $token): ?TokenInterface
{
if ($token instanceof SwitchUserToken) {
return $token->getOriginalToken();
}
return null;
}
}

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;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
use Symfony\Component\Security\Http\Firewall\ExceptionListener;
use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface;
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|FirewallListenerInterface>, ExceptionListener|null, LogoutListener|null}>
*/
private array $map = [];
/**
* @param list<callable|FirewallListenerInterface> $listeners
*
* @return void
*/
public function add(?RequestMatcherInterface $requestMatcher = null, array $listeners = [], ?ExceptionListener $exceptionListener = null, ?LogoutListener $logoutListener = null)
{
$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];
}
}

View File

@@ -0,0 +1,42 @@
<?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\FirewallListenerInterface;
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|FirewallListenerInterface>, ExceptionListener, LogoutListener}
*/
public function getListeners(Request $request);
}

View File

@@ -0,0 +1,174 @@
<?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) {
return false;
} catch (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;
}
}

View File

@@ -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 .= (str_contains($targetUri, '?') ? '&' : '?').http_build_query([$switchUserConfig['parameter'] => $identifier], '', '&');
return $targetUri;
}
}

19
vendor/symfony/security-http/LICENSE vendored Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2004-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,21 @@
<?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\LoginLink\Exception;
use Symfony\Component\Security\Core\Signature\Exception\ExpiredSignatureException;
/**
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
class ExpiredLoginLinkException extends ExpiredSignatureException implements InvalidLoginLinkExceptionInterface
{
}

View File

@@ -0,0 +1,27 @@
<?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\LoginLink\Exception;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
/**
* Thrown when a login link is invalid.
*
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
class InvalidLoginLinkAuthenticationException extends AuthenticationException
{
public function getMessageKey(): string
{
return 'Invalid or expired login link.';
}
}

View File

@@ -0,0 +1,19 @@
<?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\LoginLink\Exception;
/**
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
class InvalidLoginLinkException extends \RuntimeException implements InvalidLoginLinkExceptionInterface
{
}

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