Custom Authentication System with Guard (API Token Example)

Custom Authentication System with Guard (API Token Example)

Guard authentication can be used to:

and many more. In this example, we’ll build an API token authentication system, so we can learn more about Guard in detail.

Step 1) Prepare your User Class

Suppose you want to build an API where your clients will send an X-AUTH-TOKEN header on each request with their API token. Your job is to read this and find the associated user (if any).

First, make sure you’ve followed the main Security Guide to create your User class. Then add an apiToken property directly to your User class (the make:entity command is a good way to do this):

  1. // src/Entity/User.php
  2. namespace App\Entity;
  3. // ...
  4. class User implements UserInterface
  5. {
  6. // ...
  7. + /**
  8. + * @ORM\Column(type="string", unique=true, nullable=true)
  9. + */
  10. + private $apiToken;
  11. // the getter and setter methods
  12. }

Don’t forget to generate and run the migration:

  1. $ php bin/console make:migration
  2. $ php bin/console doctrine:migrations:migrate

Next, configure your “user provider” to use this new apiToken property:

  • YAML

    1. # config/packages/security.yaml
    2. security:
    3. # ...
    4. providers:
    5. your_db_provider:
    6. entity:
    7. class: App\Entity\User
    8. property: apiToken
    9. # ...
  • XML

    1. <!-- config/packages/security.xml -->
    2. <?xml version="1.0" encoding="UTF-8" ?>
    3. <srv:container xmlns="http://symfony.com/schema/dic/security"
    4. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    5. xmlns:srv="http://symfony.com/schema/dic/services"
    6. xsi:schemaLocation="http://symfony.com/schema/dic/services
    7. https://symfony.com/schema/dic/services/services-1.0.xsd">
    8. <config>
    9. <!-- ... -->
    10. <provider name="your_db_provider">
    11. <entity class="App\Entity\User" property="apiToken"/>
    12. </provider>
    13. <!-- ... -->
    14. </config>
    15. </srv:container>
  • PHP

    1. // config/packages/security.php
    2. $container->loadFromExtension('security', [
    3. // ...
    4. 'providers' => [
    5. 'your_db_provider' => [
    6. 'entity' => [
    7. 'class' => 'App\Entity\User',
    8. 'property' => 'apiToken',
    9. ],
    10. ],
    11. ],
    12. // ...
    13. ]);

Step 2) Create the Authenticator Class

To create a custom authentication system, create a class and make it implement Symfony\Component\Security\Guard\AuthenticatorInterface. Or, extend the simpler Symfony\Component\Security\Guard\AbstractGuardAuthenticator.

This requires you to implement several methods:

  1. // src/Security/TokenAuthenticator.php
  2. namespace App\Security;
  3. use App\Entity\User;
  4. use Doctrine\ORM\EntityManagerInterface;
  5. use Symfony\Component\HttpFoundation\JsonResponse;
  6. use Symfony\Component\HttpFoundation\Request;
  7. use Symfony\Component\HttpFoundation\Response;
  8. use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
  9. use Symfony\Component\Security\Core\Exception\AuthenticationException;
  10. use Symfony\Component\Security\Core\User\UserInterface;
  11. use Symfony\Component\Security\Core\User\UserProviderInterface;
  12. use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
  13. class TokenAuthenticator extends AbstractGuardAuthenticator
  14. {
  15. private $em;
  16. public function __construct(EntityManagerInterface $em)
  17. {
  18. $this->em = $em;
  19. }
  20. /**
  21. * Called on every request to decide if this authenticator should be
  22. * used for the request. Returning `false` will cause this authenticator
  23. * to be skipped.
  24. */
  25. public function supports(Request $request): bool
  26. {
  27. return $request->headers->has('X-AUTH-TOKEN');
  28. }
  29. /**
  30. * Called on every request. Return whatever credentials you want to
  31. * be passed to getUser() as $credentials.
  32. */
  33. public function getCredentials(Request $request)
  34. {
  35. return $request->headers->get('X-AUTH-TOKEN');
  36. }
  37. public function getUser($credentials, UserProviderInterface $userProvider): ?UserInterface
  38. {
  39. if (null === $credentials) {
  40. // The token header was empty, authentication fails with HTTP Status
  41. // Code 401 "Unauthorized"
  42. return null;
  43. }
  44. // The "username" in this case is the apiToken, see the key `property`
  45. // of `your_db_provider` in `security.yaml`.
  46. // If this returns a user, checkCredentials() is called next:
  47. return $userProvider->loadUserByUsername($credentials);
  48. }
  49. public function checkCredentials($credentials, UserInterface $user): bool
  50. {
  51. // Check credentials - e.g. make sure the password is valid.
  52. // In case of an API token, no credential check is needed.
  53. // Return `true` to cause authentication success
  54. return true;
  55. }
  56. public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey): ?Response
  57. {
  58. // on success, let the request continue
  59. return null;
  60. }
  61. public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
  62. {
  63. $data = [
  64. // you may want to customize or obfuscate the message first
  65. 'message' => strtr($exception->getMessageKey(), $exception->getMessageData())
  66. // or to translate this message
  67. // $this->translator->trans($exception->getMessageKey(), $exception->getMessageData())
  68. ];
  69. return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
  70. }
  71. /**
  72. * Called when authentication is needed, but it's not sent
  73. */
  74. public function start(Request $request, AuthenticationException $authException = null): Response
  75. {
  76. $data = [
  77. // you might translate this message
  78. 'message' => 'Authentication Required'
  79. ];
  80. return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
  81. }
  82. public function supportsRememberMe(): bool
  83. {
  84. return false;
  85. }
  86. }

Nice work! Each method is explained below: The Guard Authenticator Methods.

Step 3) Configure the Authenticator

To finish this, make sure your authenticator is registered as a service. If you’re using the default services.yaml configuration, that happens automatically.

Finally, configure your firewalls key in security.yaml to use this authenticator:

  • YAML

    1. # config/packages/security.yaml
    2. security:
    3. # ...
    4. firewalls:
    5. # ...
    6. main:
    7. anonymous: lazy
    8. logout: ~
    9. guard:
    10. authenticators:
    11. - App\Security\TokenAuthenticator
    12. # if you want, disable storing the user in the session
    13. # stateless: true
    14. # ...
  • XML

    1. <!-- config/packages/security.xml -->
    2. <?xml version="1.0" encoding="UTF-8" ?>
    3. <srv:container xmlns="http://symfony.com/schema/dic/security"
    4. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    5. xmlns:srv="http://symfony.com/schema/dic/services"
    6. xsi:schemaLocation="http://symfony.com/schema/dic/services
    7. https://symfony.com/schema/dic/services/services-1.0.xsd">
    8. <config>
    9. <!-- ... -->
    10. <!-- if you want, disable storing the user in the session
    11. add 'stateless="true"' to the firewall -->
    12. <firewall name="main" pattern="^/">
    13. <anonymous lazy="true"/>
    14. <logout/>
    15. <guard>
    16. <authenticator>App\Security\TokenAuthenticator</authenticator>
    17. </guard>
    18. <!-- ... -->
    19. </firewall>
    20. </config>
    21. </srv:container>
  • PHP

    1. // config/packages/security.php
    2. // ...
    3. use App\Security\TokenAuthenticator;
    4. $container->loadFromExtension('security', [
    5. 'firewalls' => [
    6. 'main' => [
    7. 'pattern' => '^/',
    8. 'anonymous' => 'lazy',
    9. 'logout' => true,
    10. 'guard' => [
    11. 'authenticators' => [
    12. TokenAuthenticator::class,
    13. ],
    14. ],
    15. // if you want, disable storing the user in the session
    16. // 'stateless' => true,
    17. // ...
    18. ],
    19. ],
    20. ]);

You did it! You now have a fully-working API token authentication system. If your homepage required ROLE_USER, then you could test it under different conditions:

  1. # test with no token
  2. curl http://localhost:8000/
  3. # {"message":"Authentication Required"}
  4. # test with a bad token
  5. curl -H "X-AUTH-TOKEN: FAKE" http://localhost:8000/
  6. # {"message":"Username could not be found."}
  7. # test with a working token
  8. curl -H "X-AUTH-TOKEN: REAL" http://localhost:8000/
  9. # the homepage controller is executed: the page loads normally

Now, learn more about what each method does.

The Guard Authenticator Methods

Each authenticator needs the following methods:

supports(Request $request)

This is called on every request and your job is to decide if the authenticator should be used for this request (return true) or if it should be skipped (return false).

getCredentials(Request $request)

Your job is to read the token (or whatever your “authentication” information is) from the request and return it. These credentials are passed to getUser().

getUser($credentials, UserProviderInterface $userProvider)

The $credentials argument is the value returned by getCredentials(). Your job is to return an object that implements UserInterface. If you do, then checkCredentials() will be called. If you return null (or throw an AuthenticationException) authentication will fail.

checkCredentials($credentials, UserInterface $user)

If getUser() returns a User object, this method is called. Your job is to verify if the credentials are correct. For a login form, this is where you would check that the password is correct for the user. To pass authentication, return true. If you return false (or throw an AuthenticationException), authentication will fail.

onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)

This is called after successful authentication and your job is to either return a Symfony\Component\HttpFoundation\Response object that will be sent to the client or null to continue the request (e.g. allow the route/controller to be called like normal). Since this is an API where each request authenticates itself, you want to return null.

onAuthenticationFailure(Request $request, AuthenticationException $exception)

This is called if authentication fails. Your job is to return the Symfony\Component\HttpFoundation\Response object that should be sent to the client. The $exception will tell you what went wrong during authentication.

start(Request $request, AuthenticationException $authException = null)

This is called if the client accesses a URI/resource that requires authentication, but no authentication details were sent. Your job is to return a Symfony\Component\HttpFoundation\Response object that helps the user authenticate (e.g. a 401 response that says “token is missing!”).

supportsRememberMe()

If you want to support “remember me” functionality, return true from this method. You will still need to activate remember_me under your firewall for it to work. Since this is a stateless API, you do not want to support “remember me” functionality in this example.

createAuthenticatedToken(UserInterface $user, string $providerKey)

If you are implementing the Symfony\Component\Security\Guard\AuthenticatorInterface instead of extending the Symfony\Component\Security\Guard\AbstractGuardAuthenticator class, you have to implement this method. It will be called after a successful authentication to create and return the token (a class implementing Symfony\Component\Security\Guard\Token\GuardTokenInterface) for the user, who was supplied as the first argument.

The picture below shows how Symfony calls Guard Authenticator methods:

Customizing Error Messages

When onAuthenticationFailure() is called, it is passed an AuthenticationException that describes how authentication failed via its $exception->getMessageKey() (and $exception->getMessageData()) method. The message will be different based on where authentication fails (i.e. getUser() versus checkCredentials()).

But, you can also return a custom message by throwing a Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException. You can throw this from getCredentials(), getUser() or checkCredentials() to cause a failure:

  1. // src/Security/TokenAuthenticator.php
  2. namespace App\Security;
  3. // ...
  4. use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
  5. class TokenAuthenticator extends AbstractGuardAuthenticator
  6. {
  7. // ...
  8. public function getCredentials(Request $request)
  9. {
  10. // ...
  11. if ($token == 'ILuvAPIs') {
  12. throw new CustomUserMessageAuthenticationException(
  13. 'ILuvAPIs is not a real API key: it\'s just a silly phrase'
  14. );
  15. }
  16. // ...
  17. }
  18. // ...
  19. }

In this case, since “ILuvAPIs” is a ridiculous API key, you could include an easter egg to return a custom message if someone tries this:

  1. curl -H "X-AUTH-TOKEN: ILuvAPIs" http://localhost:8000/
  2. # {"message":"ILuvAPIs is not a real API key: it's just a silly phrase"}

Manually Authenticating a User

Sometimes you might want to manually authenticate a user - like after the user completes registration. To do that, use your authenticator and a service called GuardAuthenticatorHandler:

  1. // src/Controller/RegistrationController.php
  2. namespace App\Controller;
  3. // ...
  4. use App\Security\LoginFormAuthenticator;
  5. use Symfony\Component\HttpFoundation\Request;
  6. use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
  7. class RegistrationController extends AbstractController
  8. {
  9. public function register(LoginFormAuthenticator $authenticator, GuardAuthenticatorHandler $guardHandler, Request $request): Response
  10. {
  11. // ...
  12. // after validating the user and saving them to the database
  13. // authenticate the user and use onAuthenticationSuccess on the authenticator
  14. return $guardHandler->authenticateUserAndHandleSuccess(
  15. $user, // the User object you just created
  16. $request,
  17. $authenticator, // authenticator whose onAuthenticationSuccess you want to use
  18. 'main' // the name of your firewall in security.yaml
  19. );
  20. }
  21. }

Avoid Authenticating the Browser on Every Request

If you create a Guard login system that’s used by a browser and you’re experiencing problems with your session or CSRF tokens, the cause could be bad behavior by your authenticator. When a Guard authenticator is meant to be used by a browser, you should not authenticate the user on every request. In other words, you need to make sure the supports() method only returns true when you actually need to authenticate the user. Why? Because, when supports() returns true (and authentication is ultimately successful), for security purposes, the user’s session is “migrated” to a new session id.

This is an edge-case, and unless you’re having session or CSRF token issues, you can ignore this. Here is an example of good and bad behavior:

  1. public function supports(Request $request): bool
  2. {
  3. // GOOD behavior: only authenticate (i.e. return true) on a specific route
  4. return 'login_route' === $request->attributes->get('_route') && $request->isMethod('POST');
  5. // e.g. your login system authenticates by the user's IP address
  6. // BAD behavior: So, you decide to *always* return true so that
  7. // you can check the user's IP address on every request
  8. return true;
  9. }

The problem occurs when your browser-based authenticator tries to authenticate the user on every request - like in the IP address-based example above. There are two possible fixes:

  1. If you do not need authentication to be stored in the session, set stateless: true under your firewall.
  2. Update your authenticator to avoid authentication if the user is already authenticated:
  1. // src/Security/MyIpAuthenticator.php
  2. // ...
  3. + use Symfony\Component\Security\Core\Security;
  4. class MyIpAuthenticator
  5. {
  6. + private $security;
  7. + public function __construct(Security $security)
  8. + {
  9. + $this->security = $security;
  10. + }
  11. public function supports(Request $request): bool
  12. {
  13. + // if there is already an authenticated user (likely due to the session)
  14. + // then return false and skip authentication: there is no need.
  15. + if ($this->security->getUser()) {
  16. + return false;
  17. + }
  18. + // the user is not logged in, so the authenticator should continue
  19. + return true;
  20. }
  21. }

If you use autowiring, the Security service will automatically be passed to your authenticator.

Frequently Asked Questions

Can I have Multiple Authenticators?

Yes! But when you do, you’ll need to choose only one authenticator to be your “entry_point”. This means you’ll need to choose which authenticator’s start() method should be called when an anonymous user tries to access a protected resource. For more details, see How to Use Multiple Guard Authenticators.

Can I use this with form_login?

Yes! form_login is one way to authenticate a user, so you could use it and then add one or more authenticators. Using a guard authenticator doesn’t collide with other ways to authenticate.

Can I use this with FOSUserBundle?

Yes! Actually, FOSUserBundle doesn’t handle security: it only gives you a User object and some routes and controllers to help with login, registration, forgot password, etc. When you use FOSUserBundle, you typically use form_login to actually authenticate the user. You can continue doing that (see previous question) or use the User object from FOSUserBundle and create your own authenticator(s) (like in this article).

This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.