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