Symfony 5 ApiKeyAuthenticator with SelfValidatingPassport
Asked Answered
E

3

12

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?

Eddington answered 15/9, 2021 at 10:7 Comment(1)
The documentation is quite messy, and do not provide a true example of how to make it work with a simple API Token, even if they tease us... Since UserBadge requires an UserInterface, I can't understand at allPogy
P
7

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
Pogy answered 16/3, 2022 at 9:13 Comment(2)
The implementation works well, added a few editsSpirometer
You don't need a UserProvider but could create a dummy user directly via callable (2nd param in UserBadge). See also my full answer on this page.Shiekh
C
3

You can use next validation

return new SelfValidatingPassport(
    new UserBadge($apiToken, function() use ($apiToken) {
        // TODO: here you can implement any check
    })
);
Cene answered 5/10, 2021 at 8:20 Comment(2)
+1 for the answer in general. I switched to having Users after all, to make it work.. If anyone can leave a comment and confirm this method, i'll gladly mark it as the answer. Or maybe i have another project or some spare time and then i will verify it myself.Eddington
for me its completely not possible to get it working, documentation os so obfuscated and bad written, it doesnt tell anything. api token auth is not working for meOffstage
S
2

Symfony 6.1 (But should work from Symfony 5.3)
I have multiple authenticators in my application. Form login, but also authentication via API key in the request header for the path /api. My solution is working like this:

Use separate firewall in security.yaml

api:
    stateless: true
    pattern: ^/api/
    custom_authenticators:
        - App\Security\ApiKeyAuthenticator
main:
    ...

My authenticate method in App\Security\ApiKeyAuthenticator:

public function authenticate(Request $request): Passport
{
    $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');
    }

    // Lookup whatever entity you need via some repository to check the api token.
    $event = $this->eventRepository->findOneBy(['apiToken' => $apiToken]);

    if (null === $event) {
        // fail authentication with a custom error
        throw new CustomUserMessageAuthenticationException('Invalid API token');
    }

    // *Option 1: If you have an existing User:class. Create dummy user.
    return new SelfValidatingPassport(new UserBadge($apiToken, fn() => new User()));

    // *Option 2: Use anonymous class which implements UserInterface.
    return new SelfValidatingPassport(new UserBadge($apiToken, fn() => new class implements \Symfony\Component\Security\Core\User\UserInterface {
        public function getRoles(): array { return ['ROLE_USER'];}
        public function eraseCredentials() {}
        public function getUserIdentifier(): string
        {
            return (string) Uuid::uuid4();
        }
    }));
}

App\Entity\User
Use random uuid (composer package: ramsey/uuid) as identifier for API token auth instead of email.

/**
 * A visual identifier that represents this user.
 *
 * @see UserInterface
 */
public function getUserIdentifier(): string
{
    return (string) (null !== $this->email) ? $this->email : Uuid::uuid4();
}
Shiekh answered 22/2, 2023 at 21:39 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.