Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

API Token Authenticator

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Time to put some code in our ApiTokenAuthenticator! Woo! I'm going to use Postman to help make test API requests. The only thing better than using Postman is creating functional tests in your own app. But that's the topic for another tutorial.

Let's make a GET request to http://localhost:8000/api/account. Next, how should we send the API token? As a query parameter? As a header? Well, you can do whatever you want - but using a header is pretty standard. Great! And um... what should we call that header? Postman has a nice system to help configure common authentication types. Choose something called "Bearer token". I'll show you what that means in a minute.

But first, move over to your terminal: we need to find a valid API key! Run:

php bin/console doctrine:query:sql 'SELECT * FROM api_token'

Authorization: Bearer

Copy one of these long strings, move back to Postman and paste! To see what this Auth stuff does, hit "Preview Request".

Request headers were successfully updated.

Cool! Click back to "Headers". Ahh! This "Auth" section is just a shortcut to add a request header called Authorization. Hey! Go away tooltip! Anyways, the Authorization header is set to the word "Bearer", a space, and then our token.

Honestly, you can name this header whatever you want - like SEND-ME-YOUR-TOKEN, WHATS-THE-MAGIC-WORD or I-LIKE-DINOSAURS. The name Authorization is just a standard, yea, and I guess... it does sound a bit more professional than my other ideas. There's also nothing significant about that "Bearer" part. That's another standard that's commonly used when your token is what's known as a "Bearer token": a fancy term that means whoever "bears" this token - so, whoever "possesses" this token - can use it to authenticate, without needing to provide any other types of authentication, like a master key or a password. Most API tokens, also known as "access tokens" are "bearer" tokens. And this is a standard way of attaching them to a request.

supports()

Back to work! Open ApiTokenAuthenticator. Ok: this is our second authenticator, so it's time to use our existing knowledge to kick some security butt! For supports(), our authenticator should only become active if the request has an Authorization header whose value starts with the word "Bearer". No problem: return $request->headers->has('Authorization') to make sure that header is set and also check that 0 is the position inside $request->headers->get('Authorization') where the string Bearer and a space appears:

... lines 1 - 11
class ApiTokenAuthenticator extends AbstractGuardAuthenticator
{
public function supports(Request $request)
{
// look for header "Authorization: Bearer <token>"
return $request->headers->has('Authorization')
&& 0 === strpos($request->headers->get('Authorization'), 'Bearer ');
}
... lines 20 - 57
}

I know: weird-looking code. But it does exactly what we need! If the Authorization Bearer header isn't there, supports() will return false and no other methods will be called.

getCredentials()

Next: getCredentials(). Our job is to read the token string and return it. Start with $authorizationHeader = $request->headers->get('Authorization'):

... lines 1 - 11
class ApiTokenAuthenticator extends AbstractGuardAuthenticator
{
... lines 14 - 20
public function getCredentials(Request $request)
{
$authorizationHeader = $request->headers->get('Authorization');
... lines 24 - 26
}
... lines 28 - 57
}

But, instead of returning that whole value, skip the Bearer part. So, return a sub-string of $authorizationHeader where we start at the 7th character:

... lines 1 - 11
class ApiTokenAuthenticator extends AbstractGuardAuthenticator
{
... lines 14 - 20
public function getCredentials(Request $request)
{
$authorizationHeader = $request->headers->get('Authorization');
// skip beyond "Bearer "
return substr($authorizationHeader, 7);
}
... lines 28 - 57
}

Ok. Deep breath: let's see if this is working so far. In getUser(), dump($credentials) and die:

... lines 1 - 11
class ApiTokenAuthenticator extends AbstractGuardAuthenticator
{
... lines 14 - 28
public function getUser($credentials, UserProviderInterface $userProvider)
{
dump($credentials);die;
}
... lines 33 - 57
}

This should be the API token string. Oh, and notice that this is different than LoginFormAuthenticator: we returned an array from getCredentials() there:

... lines 1 - 19
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 22 - 43
public function getCredentials(Request $request)
{
$credentials = [
'email' => $request->request->get('email'),
'password' => $request->request->get('password'),
'csrf_token' => $request->request->get('_csrf_token'),
];
... lines 51 - 56
return $credentials;
}
... lines 59 - 87
}

But that's the beauty of the authenticators: you can return whatever you want from getCredentials(). The only thing we need is the token string... so, we just return that.

Try it! Find Postman and... send! Nice! I mean, it looks terrible, but go to Preview. Yes! There is our API token string.

getUser()

Next up: getUser(). First, we need to query for the ApiToken entity. At the top of this class, make an __construct function and give it an ApiTokenRepository $apiTokenRepo argument. I'll hit Alt+Enter to initialize that:

... lines 1 - 4
use App\Repository\ApiTokenRepository;
... lines 6 - 12
class ApiTokenAuthenticator extends AbstractGuardAuthenticator
{
private $apiTokenRepo;
public function __construct(ApiTokenRepository $apiTokenRepo)
{
$this->apiTokenRepo = $apiTokenRepo;
}
... lines 21 - 73
}

Then, back in getUser(), get that token: $token = $this->apiTokenRepo->findOneBy() to query where the token property is set to the $credentials string:

... lines 1 - 12
class ApiTokenAuthenticator extends AbstractGuardAuthenticator
{
... lines 15 - 36
public function getUser($credentials, UserProviderInterface $userProvider)
{
$token = $this->apiTokenRepo->findOneBy([
'token' => $credentials
]);
... lines 42 - 47
}
... lines 49 - 73
}

If we do not find an ApiToken, return null. That will make authentication fail. If we do find one, we need to return the User, not the token. So, return $token->getUser():

... lines 1 - 12
class ApiTokenAuthenticator extends AbstractGuardAuthenticator
{
... lines 15 - 36
public function getUser($credentials, UserProviderInterface $userProvider)
{
$token = $this->apiTokenRepo->findOneBy([
'token' => $credentials
]);
if (!$token) {
return;
}
return $token->getUser();
}
... lines 49 - 73
}

Finally, if you return a User object from getUser(), Symfony calls checkCredentials(). Let's dd('checking credentials') to see if we continue to be lucky:

... lines 1 - 12
class ApiTokenAuthenticator extends AbstractGuardAuthenticator
{
... lines 15 - 49
public function checkCredentials($credentials, UserInterface $user)
{
dd('checking credentials');
}
... lines 54 - 73
}

Move back over to Postman, Send and... yes! Checking credentials.

We're almost done! But before we handle success, I want to see what happens with a bad API key. And learn how we can send back the perfect error response.

Leave a comment!

38
Login or Register to join the conversation
GDIBass Avatar
GDIBass Avatar GDIBass | posted 4 years ago | edited

If using apache don't forget to add the following to your rewrite rules:


    RewriteCond %{HTTP:Authorization} ^(.*)
    RewriteRule .* - [e=HTTP_AUTHORIZATION:%1]
3 Reply

Hey Matt,

Thanks for sharing it, though Symfony suggest a bit different rules:


    # Sets the HTTP_AUTHORIZATION header removed by Apache
    RewriteCond %{HTTP:Authorization} .
    RewriteRule ^ - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

See https://github.com/symfony/recipes-contrib/blob/f43798bc00cadc9ca6b15984277412ed99722869/symfony/apache-pack/1.0/public/.htaccess#L33-L35 . Actually, you can require symfony/apache-pack to get a complete .htaccess file for Symfony applications.

But, isn't it useful for HTTP Basic Auth only?

Cheers!

2 Reply

Thanks, man! That saved my time debugging things.

Reply
Oliver-W Avatar

for me too. although I should have been looking in this place earlier.

Reply
Duilio P. Avatar
Duilio P. Avatar Duilio P. | posted 4 years ago | edited

As mentioned below, when using Apache it is necessary to execute composer require apache-pack to get a proper .htaccess file to support the Authorization header, otherwise it won't be set in the headers object and the authorisation will fail, of course.

1 Reply

Yep! And when you are ready to deploy to production, you can follow this guide to configure your web server and speed up your application
https://symfony.com/doc/cur...

Cheers!

Reply
Dominik Avatar
Dominik Avatar Dominik | posted 4 years ago

Did I miss something? When symfony knows which "Security" class should be fired? Or both are fired and conditions are checking in both support methods?

1 Reply
Mike P. Avatar

The same question came to my mind :) Then I realised that both authenticators 'support' method will get called before the controller logic is executed.
So the APITokenAuthenticator will get called on every request (even for regular users, by example when they hit the front page) it seems.

Reply

Yep, that's correct. Unless the very first authenticator supports your request, then the next one won't be called.

Reply

Hey Dominik

You can define multiple authenticators and for each its support method will be called, if it returns true, then that's the authenticator that should return a response object.

Cheers!

Reply
Lluís F. Avatar
Lluís F. Avatar Lluís F. | posted 1 year ago | edited

ApiTokenRepsitory cannot find my entity:
<blockquote>Could not find the entity manager for class "App\Entity\ApiToken".
</blockquote>

Despite being defined:


namespace App\Repository;

use App\Entity\ApiToken;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

class ApiTokenRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
       parent::__construct($registry, ApiToken::class);
    }
}
...

Any hint?

Reply

Hey Lluís F.

Sorry for my late reply. It's likely you have a typo on your entity annotations, or perhaps you're mixing formats (PHP annotations and attributes)

Cheers!

Reply
Lluís F. Avatar
Lluís F. Avatar Lluís F. | MolloKhan | posted 1 year ago

Project was using subfolders, specified in doctrine.yaml and services.yaml; I had to modify them.
Thanks

1 Reply
Lechu85 Avatar
Lechu85 Avatar Lechu85 | posted 1 year ago

Hello from Poland :)

In symfony 5.3 I have depreciated error: Class 'AbstractGuardAuthenticator' is deprecated :/

What can I replace with this class?

Reply

Hey Lechu85,

Nice to see users from Poland here! :)

Let me give you some tips about. First of all, you can always find the class that was deprecated in the vendor/ dir and look why it was deprecated and how you can replace it. In this case, here's the deprecation message: https://github.com/symfony/... - it says that since Symfony 5.3, you should use the new authenticator system instead. To know more about the new authenticated system - you can watch a separate course on SymfonyCasts where we cover this topic in full: https://symfonycasts.com/sc... . Also, Ryan will have a workshop about it next week during the Symfony World online: https://live.symfony.com/20... - if you're going to attend it - you will know it there too :)

I hope this helps!

Cheers!

1 Reply
Lechu85 Avatar

Thank you for your response :) I dont know how use solution from Your link: https://github.com/symfony/...
, but I found something like this: https://symfony.com/doc/cur...

will it be a good solution to build this class? :)

Reply

Hey Leszek C.!

Yup, that's exactly it :). You basically just need to "convert" the logic for the Guard authenticator that we build here into the new "authenticator" system. We don't talk specifically about API authenticators, but we talk about the new authenticator system in the Symfony 5 security tutorial: https://symfonycasts.com/sc...

Cheers!

1 Reply
Anh B. Avatar

Hi, thanks for the awesome course. I have a small issue with this ApiTokenAuthenticator. No matter how I send request to the /api/account via Postman, with or without token, Im always redirected to the login page. I'm using Symfony version 5 with the new Authenticatiors. Thanks

Reply
Anh B. Avatar

Never mind, I got it working :)))

Reply

Hey @Anh!

Awesome :). I'm glad you got it figured out so quickly! Keep going!

Cheers!

Reply
Anh B. Avatar

And the other thing is if I comment the entry_point from security.yaml, it still redirects the anonymous users back to login page, I'm wondering is the entry_point really necessary here? Thanks

Reply
Default user avatar
Default user avatar Paulius | posted 2 years ago | edited

I think getCredentials() would be more readable if we return substr($authorizationHeader, strlen('Bearer '));

Reply

Hey Paulius

Sounds not very efficient, why use strlen() on known string? Maybe more readable will be to use some sort of constant?


private const HEADER_BEARER_PREFIX_LENGTH = 7;

Cheers

Reply
Simon L. Avatar
Simon L. Avatar Simon L. | posted 2 years ago

Hi there !

Great tutorial as usual, thanks :)

Can you tell me what are the main pros and cons between the API token authentication method you are building in this tuto and JWT please ?

I am currently hesitating between both. It seems that JWT is more advanced, but I can't figure out why it is better than the token in the database system...

Moreover, managing the tokens in the database allows the user to deactivate them, for example if they lose their device...

Looking forward for your reply :)

Reply

HeySimon L. !

> Great tutorial as usual, thanks :)

Cheers! :)

> Can you tell me what are the main pros and cons between the API token authentication method you are building in this tuto and JWT please ?

Excellent question! And I think you already "sense" the answer. Mostly, JWT and "tokens stored in the database" are both just... tokens. So most things are the same: you can send both on a header to authenticate in an API, but give you information about who should be logged in and potentially the "scopes", they just store that information differently (JWT stores right inside itself vs token in the database stores that info in the database). So, most things are the same, so I'll focus on the PROs/CONs of what is different:

JWT
PROs: you don't need to store your tokens. But... for many apps... storing tokens is no big deal! However, if you have a big system and you use a JWT to authentication with each system, a JWT allows each system to NOT need to make an API request back to a central auth server to ask "is this token valid?". As long as each system has the JWT public key, it can verify the signature. This is not a use-case many people have,

CONs The big con is that invalidation is much trickier. In fact, if you want to invalidate a JWT, then you actually need to keep a database storage of your *invalidated* JWT's, so that you can check it each time. And that. completely defeats the purpose.

Here's a fun read that talks about this further: https://jolicode.com/blog/w...

Let me know what you think ;).

Cheers!

1 Reply
Simon L. Avatar
Simon L. Avatar Simon L. | weaverryan | posted 2 years ago | edited

Hi weaverryan

Thank you very much, it is very clear and the link you provided is also very useful to me !

Reply

Hi, big thanks for all the awesome vids! I was trying to create a functional test for the authentication. You say; "The only thing better than using Postman is creating functional tests in your own app. But that's the topic for another tutorial.". What tutorial are you referring to?

Reply

Hey Lex,

We're talking about a completely new tutorial, like a separate one where would be covered this complex topic. Unfortunately, we don't have it yet. The only tutorials about testing we have are:

- https://symfonycasts.com/sc...
- https://symfonycasts.com/sc...

We do cover functional tests there, but not API token auth.

Cheers!

Reply
Nikolay S. Avatar
Nikolay S. Avatar Nikolay S. | posted 3 years ago

Will there be any tutorial about Two-Factor Authentication with sf4/sf5 soon?

Reply

Hey Nikolay S.!

Unfortunately, not soon - it may be something we talk about in the Symfony 5 security tutorial, but that is months away. Fortunately, I've had this conversation with someone before ;). And it includes some pointers on how to make this happen: https://symfonycasts.com/sc...

If you have any questions about that process, feel free to ask them here or there.

Cheers!

Reply
Stephan Avatar
Stephan Avatar Stephan | posted 3 years ago | edited

Hi,
In the supports function of the class ApiTokenAuthenticator, there is this line which I don't really understand:
0 === strpos($request->headers->get('Authorization'), 'Bearer ');
Can you explain me please?

Reply

Hey Stephansav,

to understand this line you need to understand how the strpos() function works. Here's the link to PHP docs about it: https://www.php.net/manual/... . So, this function returns the position of the substring we're looking for inside the given string. So, basically, if the Authorization header string contains "Bearer" subsctring in it - the strpos() function will return its position. Ans so, if the Authorization header *starts* with "Bearer" - it will return 0, i.e. 0 is the position where "Bearer" string is found in the Authorization header string. And then we just compare its position with 0.

So, in short, if the "Bearer" string is a substring of the Authorization header and it's position starts with 0, i.e Authorization header literally start with "Bearer" string - then that check (0 === strpos($request->headers->get('Authorization'), 'Bearer ')) will return true.

I hope it's clearer to you now.

Cheers!

Reply
Kamil K. Avatar
Kamil K. Avatar Kamil K. | posted 4 years ago

Can you tell me why I have an error in postman like "could not get any response"?

Reply

Hey Kamil,

Yes, I can :) It looks like you forgot to start the web server, or the host in your request URI is incorrect :) Please, double check it and make sure you can access that URI in the browser first.

Cheers!

Reply
Kamil K. Avatar

It was first thing i have checked. I started web server and i have written correct url. I have response when i change barear token.

Reply

Hey Kamil,

Hm, do you mean invalid bearer token was caused "could not get any response"? Are you using SSL from Symfony Client? Actually, below the "Could not get any response" error there's a few ideas what could cause it:


Why this might have happened:
- The server couldn't send a response: Ensure that the backend is working properly
- Self-signed SSL certificates are being blocked: Fix this by turning off 'SSL certificate verification' in Settings > General
- Proxy configured incorrectly: Ensure that proxy is configured correctly in Settings > Proxy
- Request timeout: Change request timeout in Settings > General"

I just double checked HTTPS URL by running the project with "symfony serve" command and it failed with the exact "Could not get any response" error. But after I turned on CA certs in config and specify path to it as "/Users/victor/.symfony/certs/rootCA.pem" - it works for me and I can send requests from Postman to the project HTTPS URL.

I hope this helps!

Cheers!

Reply
Nethik Avatar
Nethik Avatar Nethik | posted 4 years ago | edited

`$token = $this->apiTokenRepo->findOneBy([

            'token' => $credentials
    ]);

`

Hi Ryan,
We struggle with two things :

  1. Since you have set the token property in ApiToken entity constructor, how is it not returning it instead of the value in the database?
  2. Also, how does the ApiToken repository knows to pass the User entity to the ApiToken constructor?
    Thanks for the great screencasts!
Reply

Hey Nethik

> 1. Since you have set the token property in ApiToken entity constructor, how is it not returning it instead of the value in the database?

Because of Doctrine, Doctrine create your objects based on the data from the DB, that's why you fetch them through a repository

> 2. Also, how does the ApiToken repository knows to pass the User entity to the ApiToken constructor?

Because of the same reason :) That's just how Doctrine works. IIRC, when you fetch an object from the DB, Doctrine never executes your constructor

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-time-bundle": "^1.8", // 1.8.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.2.0
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.1.4
        "symfony/console": "^4.0", // v4.1.4
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/framework-bundle": "^4.0", // v4.1.4
        "symfony/lts": "^4@dev", // dev-master
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.1.4
        "symfony/serializer-pack": "^1.0", // v1.0.1
        "symfony/twig-bundle": "^4.0", // v4.1.4
        "symfony/web-server-bundle": "^4.0", // v4.1.4
        "symfony/yaml": "^4.0", // v4.1.4
        "twig/extensions": "^1.5" // v1.5.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/debug-bundle": "^3.3|^4.0", // v4.1.4
        "symfony/dotenv": "^4.0", // v4.1.4
        "symfony/maker-bundle": "^1.0", // v1.7.0
        "symfony/monolog-bundle": "^3.0", // v3.3.0
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.1.4
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.1.4
    }
}
userVoice