Skip to content

[Security] Implement HttpBearerAuthenticator #46429

New issue

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

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

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 Exception;
use Lcobucci\JWT\Token;
use Psr\Log\LoggerInterface;
use RuntimeException;
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\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\TokenBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\TokenPassport;
use Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
use Symfony\Component\Security\Http\TokenExtractor\BearerTokenExtractorInterface;

/**
* @author Vincent Chalamon <vincentchalamon@gmail.com>
*/
abstract class AbstractBearerAuthenticator implements AuthenticatorInterface, AuthenticationEntryPointInterface
{
protected UserProviderInterface $userProvider;

protected BearerTokenExtractorInterface $tokenExtractor;

protected string $realmName;

protected string $payloadKey;

protected LoggerInterface $logger;

public function __construct(
UserProviderInterface $userProvider,
BearerTokenExtractorInterface $tokenExtractor,
string $realmName,
string $payloadKey,
LoggerInterface $logger = null
) {
if (!interface_exists(Token::class)) {
throw new RuntimeException(sprintf('"%s" requires lcobucci/jwt, please run "composer require lcobucci/jwt" to install it.', self::class));
}

$this->userProvider = $userProvider;
$this->tokenExtractor = $tokenExtractor;
$this->realmName = $realmName;
$this->payloadKey = $payloadKey;
$this->logger = $logger;
}

public function start(Request $request, AuthenticationException $authException = null): Response
{
$response = new Response();
$response->headers->set('WWW-Authenticate', sprintf('Bearer realm="%s"', $this->realmName));
$response->setStatusCode(Response::HTTP_UNAUTHORIZED);

return $response;
}

public function supports(Request $request): ?bool
{
return $this->tokenExtractor->supports($request);
}

public function authenticate(Request $request): Passport
{
try {
$token = $this->getToken($this->tokenExtractor->extract($request));
} catch (Exception $exception) {
throw new AuthenticationException($exception->getMessage(), $exception->getCode(), $exception);
}

$userIdentifier = $token->claims()->get($this->payloadKey);
if (null === $userIdentifier) {
throw new AuthenticationException(sprintf('Cannot retrieve key "%s" from token.', $this->payloadKey));
}

return new TokenPassport(
new TokenBadge($token, $userIdentifier, [$this->userProvider, 'loadUserByIdentifier'])
);
}

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

public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
if (null !== $this->logger) {
$this->logger->info('Bearer authentication failed for token.', ['token' => $this->tokenExtractor->extract($request), 'exception' => $exception]);
}

return $this->start($request, $exception);
}

abstract protected function getToken(string $data): Token;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Configuration;

use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Signer\Hmac\Sha384;
use Lcobucci\JWT\Signer\Hmac\Sha512;
use Lcobucci\JWT\Signer\Key\InMemory;
use Symfony\Contracts\HttpClient\HttpClientInterface;

/**
* todo Migrate this factory to framework bundle configuration.
*
* @author Vincent Chalamon <vincentchalamon@gmail.com>
*
* @final
*/
class ConfigurationFactory
{
/**
* @var array<string, class-string<Signer>>
*/
final public const SIGN_ALGORITHMS = [
'HS256' => Sha256::class,
'HS384' => Sha384::class,
'HS512' => Sha512::class,
'ES256' => Signer\Ecdsa\Sha256::class,
'ES384' => Signer\Ecdsa\Sha384::class,
'ES512' => Signer\Ecdsa\Sha512::class,
'RS256' => Signer\Rsa\Sha256::class,
'RS384' => Signer\Rsa\Sha384::class,
'RS512' => Signer\Rsa\Sha512::class,
];

public static function createFromBase64Encoded(string $algorithm, string $key): Configuration
{
$signerClass = self::SIGN_ALGORITHMS[$algorithm];

return Configuration::forSymmetricSigner(new $signerClass(), InMemory::base64Encoded($key));
}

public static function createFromFile(string $algorithm, string $key): Configuration
{
$signerClass = self::SIGN_ALGORITHMS[$algorithm];

return Configuration::forSymmetricSigner(new $signerClass(), InMemory::file($key));
}

public static function createFromPlainText(string $algorithm, string $key): Configuration
{
$signerClass = self::SIGN_ALGORITHMS[$algorithm];

return Configuration::forSymmetricSigner(new $signerClass(), InMemory::plainText($key));
}

public static function createFromUri(string $algorithm, string $key, HttpClientInterface $client = null): Configuration
{
$signerClass = self::SIGN_ALGORITHMS[$algorithm];

return Configuration::forSymmetricSigner(new $signerClass(), InMemory::plainText(sprintf(<<<KEY
-----BEGIN PUBLIC KEY-----
%s
-----END PUBLIC KEY-----
KEY
, $client->request('GET', $key)->toArray()['public_key']
)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Token;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\SignedTokenBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\TokenBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\TokenExtractor\BearerTokenExtractorInterface;

/**
* @author Vincent Chalamon <vincentchalamon@gmail.com>
*
* @final
*/
class HttpBearerAuthenticator extends AbstractBearerAuthenticator
{
private readonly Configuration $configuration;

public function __construct(UserProviderInterface $userProvider, BearerTokenExtractorInterface $tokenExtractor, Configuration $configuration, string $realmName, string $payloadKey, LoggerInterface $logger = null)
{
parent::__construct($userProvider, $tokenExtractor, $realmName, $payloadKey, $logger);

$this->configuration = $configuration;
}

/**
* Override Passport to add SignedTokenBadge.
*/
public function authenticate(Request $request): Passport
{
$passport = parent::authenticate($request);

return $passport->addBadge(new SignedTokenBadge($this->configuration, $passport->getBadge(TokenBadge::class)->getToken()));
}

protected function getToken(string $data): Token
{
return $this->configuration->parser()->parse($data);
}
}
Original file line number Diff line number Diff line change
@@ -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\Passport\Badge;

use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Token;

/**
* @author Vincent Chalamon <vincentchalamon@gmail.com>
*
* @final
*/
class SignedTokenBadge implements BadgeInterface
{
private readonly Configuration $configuration;

private readonly Token $payload;

public function __construct(Configuration $configuration, Token $token)
{
$this->configuration = $configuration;
$this->payload = $token;
}

public function getConfiguration(): Configuration
{
return $this->configuration;
}

public function getToken(): Token
{
return $this->payload;
}

public function isResolved(): bool
{
return true;
}
}
Original file line number Diff line number Diff line change
@@ -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 Lcobucci\JWT\Token;
use LogicException;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\AuthenticationServiceException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\EventListener\TokenUserLoaderListener;

/**
* @author Vincent Chalamon <vincentchalamon@gmail.com>
*
* @final
*/
class TokenBadge extends UserBadge
{
private readonly Token $payload;

private ?UserInterface $user = null;

public function __construct(Token $token, string $userIdentifier, callable $userLoader = null)
{
parent::__construct($userIdentifier, $userLoader);

$this->payload = $token;
}

public function getToken(): Token
{
return $this->payload;
}

/**
* @throws AuthenticationException when the user cannot be found
*/
public function getUser(): UserInterface
{
if (!isset($this->user)) {
if (null === $this->getUserLoader()) {
throw new LogicException(sprintf('No user loader is configured, did you forget to register the "%s" listener?', TokenUserLoaderListener::class));
}

$this->user = ($this->getUserLoader())($this->getUserIdentifier(), $this->getToken());
if (!$this->user instanceof UserInterface) {
throw new AuthenticationServiceException(sprintf('The user provider must return a UserInterface object, "%s" given.', get_debug_type($this->user)));
}
}

return $this->user;
}
}
Loading