welcome back to dyb-tech
This commit is contained in:
@@ -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,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;
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\AccessToken\Oidc\Exception;
|
||||
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
|
||||
/**
|
||||
* This exception is thrown when the token signature is invalid.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
class InvalidSignatureException extends AuthenticationException
|
||||
{
|
||||
public function getMessageKey(): string
|
||||
{
|
||||
return 'Invalid token signature.';
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\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).
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
class MissingClaimException extends AuthenticationException
|
||||
{
|
||||
public function getMessageKey(): string
|
||||
{
|
||||
return 'Missing claim.';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\AccessToken\Oidc;
|
||||
|
||||
use Jose\Component\Checker;
|
||||
use Jose\Component\Checker\ClaimCheckerManager;
|
||||
use Jose\Component\Core\Algorithm;
|
||||
use Jose\Component\Core\AlgorithmManager;
|
||||
use Jose\Component\Core\JWK;
|
||||
use Jose\Component\Signature\JWSTokenSupport;
|
||||
use Jose\Component\Signature\JWSVerifier;
|
||||
use Jose\Component\Signature\Serializer\CompactSerializer;
|
||||
use Jose\Component\Signature\Serializer\JWSSerializerManager;
|
||||
use Psr\Clock\ClockInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Clock\Clock;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface;
|
||||
use Symfony\Component\Security\Http\AccessToken\Oidc\Exception\InvalidSignatureException;
|
||||
use Symfony\Component\Security\Http\AccessToken\Oidc\Exception\MissingClaimException;
|
||||
use Symfony\Component\Security\Http\Authenticator\FallbackUserLoader;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
||||
|
||||
/**
|
||||
* The token handler decodes and validates the token, and retrieves the user identifier from it.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
final class OidcTokenHandler implements AccessTokenHandlerInterface
|
||||
{
|
||||
use OidcTrait;
|
||||
|
||||
public function __construct(
|
||||
private Algorithm $signatureAlgorithm,
|
||||
private JWK $jwk,
|
||||
private string $audience,
|
||||
private array $issuers,
|
||||
private string $claim = 'sub',
|
||||
private ?LoggerInterface $logger = null,
|
||||
private ClockInterface $clock = new Clock()
|
||||
) {
|
||||
}
|
||||
|
||||
public function getUserBadgeFrom(string $accessToken): UserBadge
|
||||
{
|
||||
if (!class_exists(JWSVerifier::class) || !class_exists(Checker\HeaderCheckerManager::class)) {
|
||||
throw new \LogicException('You cannot use the "oidc" token handler since "web-token/jwt-signature" and "web-token/jwt-checker" are not installed. Try running "composer require web-token/jwt-signature web-token/jwt-checker".');
|
||||
}
|
||||
|
||||
try {
|
||||
// Decode the token
|
||||
$jwsVerifier = new JWSVerifier(new AlgorithmManager([$this->signatureAlgorithm]));
|
||||
$serializerManager = new JWSSerializerManager([new CompactSerializer()]);
|
||||
$jws = $serializerManager->unserialize($accessToken);
|
||||
$claims = json_decode($jws->getPayload(), true);
|
||||
|
||||
// Verify the signature
|
||||
if (!$jwsVerifier->verifyWithKey($jws, $this->jwk, 0)) {
|
||||
throw new InvalidSignatureException();
|
||||
}
|
||||
|
||||
// Verify the headers
|
||||
$headerCheckerManager = new Checker\HeaderCheckerManager([
|
||||
new Checker\AlgorithmChecker([$this->signatureAlgorithm->name()]),
|
||||
], [
|
||||
new JWSTokenSupport(),
|
||||
]);
|
||||
// if this check fails, an InvalidHeaderException is thrown
|
||||
$headerCheckerManager->check($jws, 0);
|
||||
|
||||
// Verify the claims
|
||||
$checkers = [
|
||||
new Checker\IssuedAtChecker(0, false, $this->clock),
|
||||
new Checker\NotBeforeChecker(0, false, $this->clock),
|
||||
new Checker\ExpirationTimeChecker(0, false, $this->clock),
|
||||
new Checker\AudienceChecker($this->audience),
|
||||
new Checker\IssuerChecker($this->issuers),
|
||||
];
|
||||
$claimCheckerManager = new ClaimCheckerManager($checkers);
|
||||
// if this check fails, an InvalidClaimException is thrown
|
||||
$claimCheckerManager->check($claims);
|
||||
|
||||
if (empty($claims[$this->claim])) {
|
||||
throw new MissingClaimException(sprintf('"%s" claim not found.', $this->claim));
|
||||
}
|
||||
|
||||
// UserLoader argument can be overridden by a UserProvider on AccessTokenAuthenticator::authenticate
|
||||
return new UserBadge($claims[$this->claim], new FallbackUserLoader(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::class)) {
|
||||
throw new \LogicException('You cannot use the "OidcUserInfoTokenHandler" since the String component is not installed. Try running "composer require symfony/string".');
|
||||
}
|
||||
|
||||
foreach ($claims as $claim => $value) {
|
||||
unset($claims[$claim]);
|
||||
if ('' === $value || null === $value) {
|
||||
continue;
|
||||
}
|
||||
$claims[u($claim)->camel()->toString()] = $value;
|
||||
}
|
||||
|
||||
if (isset($claims['updatedAt']) && '' !== $claims['updatedAt']) {
|
||||
$claims['updatedAt'] = (new \DateTimeImmutable())->setTimestamp($claims['updatedAt']);
|
||||
}
|
||||
|
||||
if (\array_key_exists('emailVerified', $claims) && null !== $claims['emailVerified'] && '' !== $claims['emailVerified']) {
|
||||
$claims['emailVerified'] = (bool) $claims['emailVerified'];
|
||||
}
|
||||
|
||||
if (\array_key_exists('phoneNumberVerified', $claims) && null !== $claims['phoneNumberVerified'] && '' !== $claims['phoneNumberVerified']) {
|
||||
$claims['phoneNumberVerified'] = (bool) $claims['phoneNumberVerified'];
|
||||
}
|
||||
|
||||
return new OidcUser(...$claims);
|
||||
}
|
||||
}
|
||||
@@ -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\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.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user