<?php
declare(strict_types=1);
namespace League\Bundle\OAuth2ServerBundle\Security\Authenticator;
use League\Bundle\OAuth2ServerBundle\Security\Authentication\Token\OAuth2Token;
use League\Bundle\OAuth2ServerBundle\Security\Exception\OAuth2AuthenticationException;
use League\Bundle\OAuth2ServerBundle\Security\Exception\OAuth2AuthenticationFailedException;
use League\Bundle\OAuth2ServerBundle\Security\Passport\Badge\ScopeBadge;
use League\Bundle\OAuth2ServerBundle\Security\User\NullUser;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\ResourceServer;
use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface;
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\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
/**
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
*/
final class OAuth2Authenticator implements AuthenticatorInterface, AuthenticationEntryPointInterface
{
/**
* @psalm-suppress UndefinedTrait
* @psalm-suppress MethodSignatureMismatch
*/
use ForwardCompatAuthenticatorTrait;
private HttpMessageFactoryInterface $httpMessageFactory;
private ResourceServer $resourceServer;
private UserProviderInterface $userProvider;
private string $rolePrefix;
public function __construct(
HttpMessageFactoryInterface $httpMessageFactory,
ResourceServer $resourceServer,
UserProviderInterface $userProvider,
string $rolePrefix
) {
$this->httpMessageFactory = $httpMessageFactory;
$this->resourceServer = $resourceServer;
$this->userProvider = $userProvider;
$this->rolePrefix = $rolePrefix;
}
public function supports(Request $request): ?bool
{
return str_starts_with($request->headers->get('Authorization', ''), 'Bearer ');
}
public function start(Request $request, ?AuthenticationException $authException = null): Response
{
return new Response('', 401, ['WWW-Authenticate' => 'Bearer']);
}
/**
* @return Passport
*/
public function doAuthenticate(Request $request) /* : Passport */
{
try {
$psr7Request = $this->resourceServer->validateAuthenticatedRequest($this->httpMessageFactory->createRequest($request));
} catch (OAuthServerException $e) {
throw OAuth2AuthenticationFailedException::create('The resource server rejected the request.', $e);
}
/** @var string $userIdentifier */
$userIdentifier = $psr7Request->getAttribute('oauth_user_id', '');
/** @var string $accessTokenId */
$accessTokenId = $psr7Request->getAttribute('oauth_access_token_id');
/** @var list<string> $scopes */
$scopes = $psr7Request->getAttribute('oauth_scopes', []);
/** @var string $oauthClientId */
$oauthClientId = $psr7Request->getAttribute('oauth_client_id', '');
/** @psalm-suppress MixedInferredReturnType */
$userLoader = function (string $userIdentifier): UserInterface {
if ('' === $userIdentifier) {
return new NullUser();
}
if (!method_exists($this->userProvider, 'loadUserByIdentifier')) {
/**
* @psalm-suppress DeprecatedMethod
* @psalm-suppress MixedReturnStatement
*/
return $this->userProvider->loadUserByUsername($userIdentifier);
}
return $this->userProvider->loadUserByIdentifier($userIdentifier);
};
$passport = new SelfValidatingPassport(new UserBadge($userIdentifier, $userLoader), [
new ScopeBadge($scopes),
]);
$passport->setAttribute('accessTokenId', $accessTokenId);
$passport->setAttribute('oauthClientId', $oauthClientId);
return $passport;
}
/**
* @return OAuth2Token
*
* @psalm-suppress DeprecatedInterface
* @psalm-suppress UndefinedClass
* @psalm-suppress MixedInferredReturnType
* @psalm-suppress RedundantCondition
*/
public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface
{
if (!$passport instanceof Passport) {
throw new \RuntimeException(sprintf('Cannot create a OAuth2 authenticated token. $passport should be a %s', Passport::class));
}
$token = $this->createToken($passport, $firewallName);
/**
* @psalm-suppress TooManyArguments
* @psalm-suppress UndefinedMethod
*/
$token->setAuthenticated(true);
return $token;
}
/**
* @return OAuth2Token
*/
public function createToken(Passport $passport, string $firewallName): TokenInterface
{
/** @var string $accessTokenId */
$accessTokenId = $passport->getAttribute('accessTokenId');
/** @var ScopeBadge $scopeBadge */
$scopeBadge = $passport->getBadge(ScopeBadge::class);
/** @var string $oauthClientId */
$oauthClientId = $passport->getAttribute('oauthClientId', '');
$token = new OAuth2Token($passport->getUser(), $accessTokenId, $oauthClientId, $scopeBadge->getScopes(), $this->rolePrefix);
if (method_exists(AuthenticatorInterface::class, 'createAuthenticatedToken') && !method_exists(AuthenticatorInterface::class, 'createToken')) {
// symfony 5.4 only
/**
* @psalm-suppress TooManyArguments
* @psalm-suppress UndefinedMethod
*/
$token->setAuthenticated(true, false);
}
return $token;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
if ($exception instanceof OAuth2AuthenticationException) {
return new Response($exception->getMessage(), $exception->getStatusCode(), $exception->getHeaders());
}
throw $exception;
}
}