Symfony 5 ApiKeyAuthenticator with SelfValidatingPassport

Viewed 2368

I am working on a new Symfony 5.3.6 project and want to implement authentication, based on the new system as stated in:

https://symfony.com/doc/current/security/authenticator_manager.html#creating-a-custom-authenticator

I do not have any users and just want to check if the sent api token is correct, so when implementing this method:

public function authenticate(Request $request): PassportInterface
{
    $apiToken = $request->headers->get('X-AUTH-TOKEN');

    if (null === $apiToken) {
        // The token header was empty, authentication fails with HTTP Status Code 401 "Unauthorized"
        throw new CustomUserMessageAuthenticationException('No API token provided');
    }

    return new SelfValidatingPassport(new UserBadge($apiToken));
}

where exactly is the checking done? Have i forgotten to implement another Class somewhere?

If I leave the code as is it lands directly in onAuthenticationFailure.

I understand, that I could implement Users/UserProvider with an attribute $apiToken and then the system would check if the database entry corresponds with the token in the request. But i do not have users.

It should be possible without having users, because on the above URL, it says:

Self Validating Passport

If you don’t need any credentials to be checked (e.g. when using API tokens), you can use the Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport. This class only requires a UserBadge object and optionally Passport Badges.

But that is a little thin. How do I "use" it?

2 Answers

Ok, I think I got the point, in any case, you need to handle some User & then you need to create a customer Userprovider.

Here my logic:

App\Security\UserProvider:

class UserProvider implements UserProviderInterface, PasswordUpgraderInterface
{
    public function loadUserByIdentifier($identifier): UserInterface
    {
        if ($identifier === 'YOUR_API_KEY') {
            return new User();
        }

        throw new UserNotFoundException('API Key is not correct');
    }
    ...

App\Security\ApiKeyAuthenticator:

class ApiKeyAuthenticator extends AbstractAuthenticator
{
    private UserProvider $userProvider;

    public function __construct(UserProvider $userProvider)
    {
        $this->userProvider = $userProvider;
    }

    public function supports(Request $request): ?bool
    {
        // allow api docs page
        return trim($request->getPathInfo(), '/') !== 'docs';
    }

    public function authenticate(Request $request): Passport
    {
        $apiToken = $request->headers->get('X-API-KEY');
        if (null === $apiToken) {
            // The token header was empty, authentication fails with HTTP Status
            // Code 401 "Unauthorized"
            throw new CustomUserMessageAuthenticationException('No API token provided');
        }

        return new SelfValidatingPassport(
            new UserBadge($apiToken, function () use ($apiToken) {
                return $this->userProvider->loadUserByIdentifier($apiToken);
            })
        );
    }

It works for me, my API is protected by a basic API Key in the header. I don't know if it's the best way, but seems ok.

And define in your security.yaml:

providers:
    # used to reload user from session & other features (e.g. switch_user)
    app_user_provider:
        id: App\Security\UserProvider

You can use next validation

return new SelfValidatingPassport(
    new UserBadge($apiToken, function() use ($apiToken) {
        // TODO: here you can implement any check
    })
);
Related