11

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?

connectedMind
  • 421
  • 4
  • 17
  • 2
    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 all – Vincent Decaux Mar 16 '22 at 08:48

3 Answers3

6

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
Vincent Decaux
  • 9,857
  • 6
  • 56
  • 84
  • The implementation works well, added a few edits – Acute X Jul 26 '22 at 11:07
  • 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. – segli Feb 22 '23 at 21:41
3

You can use next validation

return new SelfValidatingPassport(
    new UserBadge($apiToken, function() use ($apiToken) {
        // TODO: here you can implement any check
    })
);
qnixdev
  • 41
  • 5
  • +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. – connectedMind Oct 13 '21 at 08:19
  • 3
    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 me – Karol Pawlak Nov 10 '21 at 10:37
1

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();
}
segli
  • 230
  • 2
  • 6