Skip to content

Extract LexikJWTAuthenticationBundle UserBadge logic to be used with Symfony native AccessTokenAuthenticator #1301

@ambroisemaupate

Description

@ambroisemaupate

Today JWTAuthenticator is a monolitic AbstractAuthenticator which:

  • Extracts JWT from request
  • Parse and validate JWT
  • Creates UserBadge
  • Creates SelfValidatingPassport

Since 6.4, Symfony can authenticate an AccessToken and already handles the SelfValidatingPassport part natively: https://symfony.com/doc/6.4/security/access_token.html


Symfony's Symfony\Component\Security\Http\Authenticator\AccessTokenAuthenticator already:

  • Extracts JWT from request
  • Delegates UserBadge creation to a AccessTokenHandlerInterface
  • Creates SelfValidatingPassport

Could we split the authentication logic into 2 parts:

  • An AccessTokenHandlerInterface implementation
  • Keep JWTAuthenticator which will use AccessTokenHandlerInterface implementation for backward compatibility

This will allow to use native Symfony AccessTokenAuthenticator but with lexik AccessTokenHandlerInterface implementation.

For example:

<?php

declare(strict_types=1);

namespace App\Security\AccessToken;

use Lexik\Bundle\JWTAuthenticationBundle\Exception\ExpiredTokenException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\InvalidPayloadException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\InvalidTokenException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
use Lexik\Bundle\JWTAuthenticationBundle\Security\User\PayloadAwareUserProviderInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\ChainUserProvider;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;

/*
 * Extract LexikJWTAuthenticationBundle UserBadge logic to be used with Symfony Access Token system.
 */
final readonly class JWTTokenHandler implements AccessTokenHandlerInterface
{
    public function __construct(
        #[Autowire(service: 'security.user_providers')]
        private UserProviderInterface $userProvider,
        private JWTTokenManagerInterface $jwtManager,
    ) {
    }

    public function getUserBadgeFrom(#[\SensitiveParameter] string $accessToken): UserBadge
    {
        try {
            if (!$payload = $this->jwtManager->parse($accessToken)) {
                throw new InvalidTokenException('Invalid JWT Token');
            }
        } catch (JWTDecodeFailureException $e) {
            if (JWTDecodeFailureException::EXPIRED_TOKEN === $e->getReason()) {
                throw new ExpiredTokenException();
            }

            throw new InvalidTokenException('Invalid JWT Token', 0, $e);
        }

        $idClaim = $this->jwtManager->getUserIdClaim();
        if (!isset($payload[$idClaim])) {
            throw new InvalidPayloadException($idClaim);
        }

        return new UserBadge(
            (string) $payload[$idClaim],
            fn ($userIdentifier) => $this->loadUser($payload, $userIdentifier)
        );
    }

    /**
     * Loads the user to authenticate.
     *
     * @param array                 $payload      The token payload
     * @param string                $identity     The key from which to retrieve the user "identifier"
     */
    protected function loadUser(array $payload, string $identity): UserInterface
    {
        if ($this->userProvider instanceof PayloadAwareUserProviderInterface) {
            return $this->userProvider->loadUserByIdentifierAndPayload($identity, $payload);
        }

        if ($this->userProvider instanceof ChainUserProvider) {
            foreach ($this->userProvider->getProviders() as $provider) {
                try {
                    if ($provider instanceof PayloadAwareUserProviderInterface) {
                        return $provider->loadUserByIdentifierAndPayload($identity, $payload);
                    }

                    return $provider->loadUserByIdentifier($identity);
                } catch (AuthenticationException $e) {
                    // try next one
                }
            }

            $ex = new UserNotFoundException(sprintf('There is no user with identifier "%s".', $identity));
            $ex->setUserIdentifier($identity);

            throw $ex;
        }

        return $this->userProvider->loadUserByIdentifier($identity);
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions