Buy

JWT: Other Things to Think about

Mostly, that's it! JWT authentication is pretty cool: create an endpoint to fetch a token and an authenticator to check if that token is valid. With the error handling we added, this is a really robust system.

But, there are a few other things I want you to think about: things that you may want to consider for your situation.

Adding Scopes/Roles to the Token

First, when we created our JWT, we put the username inside of it:

41 lines src/AppBundle/Controller/Api/TokenController.php
... lines 1 - 12
class TokenController extends BaseController
{
... lines 15 - 18
public function newTokenAction(Request $request)
{
$user = $this->getDoctrine()
->getRepository('AppBundle:User')
->findOneBy(['username' => $request->getUser()]);
if (!$user) {
throw $this->createNotFoundException();
}
$isValid = $this->get('security.password_encoder')
->isPasswordValid($user, $request->getPassword());
if (!$isValid) {
throw new BadCredentialsException();
}
$token = $this->get('lexik_jwt_authentication.encoder')
->encode(['username' => $user->getUsername()]);
return new JsonResponse(['token' => $token]);
}
}

Later, we used that to query for the User object:

101 lines src/AppBundle/Security/JwtTokenAuthenticator.php
... lines 1 - 19
class JwtTokenAuthenticator extends AbstractGuardAuthenticator
{
... lines 22 - 48
public function getUser($credentials, UserProviderInterface $userProvider)
{
$data = $this->jwtEncoder->decode($credentials);
if ($data === false) {
throw new CustomUserMessageAuthenticationException('Invalid Token');
}
$username = $data['username'];
return $this->em
->getRepository('AppBundle:User')
->findOneBy(['username' => $username]);
}
... lines 63 - 99
}

But, you can put any information in your token. In fact, you could also include "scopes" - or "roles" to use a more Symfony-ish word - inside your token. Also, nobody is forcing your authenticator to load a user from the database. To get really crazy, you could decode the token and create some new, non-entity User object, and populate it entirely from the information inside of that token.

And really, not everyone issues tokens that are related to a specific user in their system. Sometimes, tokens are more like a package of permissions that describe what an API client can and can't do. This is a powerful idea.

OAuth versus JWT

And what about OAuth? If you've watched our OAuth tutorial, then you remember that OAuth is just a mechanism for securely delivering a token to an API client. You may or may not need OAuth for your app, but if you do use it, you still have the option to use JSON web tokens as your bearer, or access tokens. It's not an OAuth versus JWT thing: each accomplishes different goals.

Refresh Tokens

Finally, let's talk refresh tokens. In our app, we gave our tokens a lifetime of 1 hour:

77 lines app/config/config.yml
... lines 1 - 72
lexik_jwt_authentication:
... lines 74 - 76
token_ttl: 3600

You see, JWT's aren't supposed to last forever. If you need them to, you might choose to issue a refresh token along with your normal access token. Then later, an API client could send the refresh token to the server and exchange it for a new JWT access token. Implementing this is pretty easy: it involves creating an extra token and an endpoint for exchanging it later. Auth0 - a leader in JWT - has a nice blog post about it.

Ok! If you have any questions, let me know. I know this stuff can be crazy confusing! Do your best to not overcomplicate things.

And as always, I'll see you guys next time.

Leave a comment!

  • 2018-05-21 weaverryan

    Hey @Simon!

    Cool question :). Basically, no, you don't need JWT :). The advantage of JWT is that you don't need to store the token string in the database. And this is especially helpful if you have a distributed system. For example, suppose you have an "auth" server that you use to "get" your JWT. But then, you use that JWT to make API requests to "serverA, "serverB" and "serverC". With JWT, each server can verify the validity of the JWT *without* needing to contact the "auth" server. They can do this just by verifying the signature on the JWT. This gives you a more distributed system, and, if you have this setup, faster, because "serverA" doesn't need to talk to the "auth" server to check info about the token.

    This is cool... but it's also not a situation that many people have :). So if you do not have this situation, then the advantage of JWT is simply that you do not need to store the API token in the database. But... that's not too hard really :). And, it also makes it easier to "logout" - as you can just delete that API token or mark it as "logged out" or expired.

    So yea, totally feel free to generate your own tokens (that are not JWT), store them in the database, implement things in Guard, and then give yourself a high-five :)

    Cheers!

  • 2018-05-20 Simon

    Why should I use JWT tokens instead of api keys (Symfony)

    I have implement a Guard Authentication
    https://symfony.com/doc/cur...
    with api keys.

    Here in addition lexikJWT is used
    https://knpuniversity.com/s...

    On my test system, lexikJWT is very slow. Do I really
    need JWT? When I implement addUser, logout and expire
    for guard I will have everything I need, or?

    - In both cases, others cant modify the apiKey or
    JWT token (then it is invalid) or produce new ones.
    - If somebody stole apiKey or JWT token, he will
    have access in both cases.
    - With the apiKey there is also a role and
    a unique name/id in the database, so I dont need
    this informations in a JWT token.

    So where is the advantage?

    Disadvantage in my option is
    - lexikJWT is much slower, especially in a service
    orientated achitecture with many different api calls...
    - lexikJWT is third party software (how knows if
    lexikJWT exist or is well supported in 4 years)

    Thank you & Best Regards

  • 2018-04-30 weaverryan

    Hey Mat!

    Let me see if I can help make things a bit more clear :). First, I DO think that using https://github.com/gesdinet... is your best bet. OR, possibly, instead of using, "stealing" from it.

    So let's talk about the "requirements". They are basically this:

    1) Yes, you will have a "RefreshToken" entity. And, you can basically see what this needs to have on it here: https://github.com/gesdinet... - just (A) the refresh token itself, (B) a username or user id if you want, and (C) a date for the refresh token to expire. Actually, you'll notice in that bundle that the refresh token is not a JWT: it's just a random token. And so, instead of the data being stored IN the token, the token is meaningless, and the data (e.g. username, valid time) is stored in the database. This isn't a requirement. But, because refresh tokens MUST be stored in the database, there's no real benefit to using JWT: you might as well just store all the data in the database and have full control over it.

    2) When the user makes an API request to get an JWT, you'll now also return a refresh token automatically (and yes, you'll save this into the database).

    3) Finally, you will create a new endpoint - e.g. /api/token/refresh. You are able to send a refresh token to this endpoint (e.g. a POST request, with the token either as the entire body of the request, or as a POST field called refresh_token). We will read that refresh token, and if it is valid, return a new JWT. Optionally, you can also return a new refresh token, or increase the "valid" date of the existing refresh token.

    And... that's it! I would probably do (3) as an authenticator, but it could also be a normal controller endpoint. https://github.com/gesdinet... is quite good, but I find it also a bit confusing at the same time. Overall, the problem is *fairly* simple, and I hope this clarifies.

    Cheers!

  • 2018-04-27 Mat

    Hey guys!

    I have one question. How do I implement a refresh token in this system. Because you said it's quite easy to implement, I'm struggling for days with it and with no chance of success, could you give us some hints? This tutorial is so good to understand.

    I'm quite helpless how to proceed. I already looked into https://github.com/gesdinet..., since Lexik JWT bundle recommends this for refresh token implementation. But regardless of my effort, I do not know how to implement this into my system.

    When I hit the /api/tokens route should I generate beside the access token a new Refresh token, and store it in a database? Is a UUID enough?

    Then in JWTTokenAuthenticator. php in getUser() is this a valid entry point to do some action when the Token is expired? I.e. test for the refresh token.


    public function getUser($credentials, UserProviderInterface $userProvider)
    {
    try {
    $data = $this->jwtEncoder->decode($credentials);
    } catch (JWTDecodeFailureException $e) {

    switch ($e) {
    case $e::EXPIRED_TOKEN;
    // TODO - call some Action if AccessToken is expired
    break;
    default:
    throw new CustomUserMessageAuthenticationException('Invalid Token');
    }
    }
    $username = $data['username'];
    return $this->em
    ->getRepository('AppBundle:User')
    ->findOneBy(['username' => $username]);
    }

    I hope you can help me. A follow up video would be so much appreciated. Thanks guys :)

  • 2018-04-27 Mat

    Hey @Knp!

    I have one question. How do I implement a refresh token in this system. Because you said it's quite easy to implement, I'm struggling for days with it and with no chance of success, could you give us some hints? This tutorial is so good to understand.

    I'm quite helpless how to proceed. I already looked into https://github.com/gesdinet..., since Lexik JWT bundle recommends this for refresh token implementation. But regardless of my effort, I do not know how to implement this into my system.

    When I hit the /api/tokens route should I generate beside the access token a new Refresh token, and store it in a database? Is a UUID enough?

    Then in JWTTokenAuthenticator. php in getUser() is this a valid entry point to do some action when the Token is expired? I.e. test for the refresh token.


    public function getUser($credentials, UserProviderInterface $userProvider)
    {
    try {
    $data = $this->jwtEncoder->decode($credentials);
    } catch (JWTDecodeFailureException $e) {

    switch ($e) {
    case $e::EXPIRED_TOKEN;
    // TODO - call some Action if AccessToken is expired
    break;
    default:
    throw new CustomUserMessageAuthenticationException('Invalid Token');
    }
    }
    $username = $data['username'];
    return $this->em
    ->getRepository('AppBundle:User')
    ->findOneBy(['username' => $username]);
    }

    I hope you can help me. A follow up video would be so much appreciated. Thanks guys :)

  • 2017-11-07 weaverryan

    Hey Ahmed Bhs!

    Excellent question! Actually, you have a 2 main options:

    1) If you want, you can create a nice authentication system with NO API token system at all. Instead, you can create a traditional form login system and submit the POST request. You'll just need to make sure your form login system does not redirect (like a normal form system does), and instead returns some nice JSON response (the JSON response can be anything, but maybe it will be useful for you to return some user data - but that doesn't really matter). In this system, Symfony will set a session cookie just like with a normal, non-AJAX login. All future AJAX requests will automatically use this cookie to authenticate. I recommend this approach in a lot of cases... because it's really simple. If your API exists *only* so that your own JavaScript frontend can talk to it, then this is great.

    2) If you do need/want to use API tokens for some reason, then the "flow" would look something like this:

    A) When the user logs in, send a POST request with the username & password to some endpoint that will check the validity and return an access token. We do this exactly here: https://knpuniversity.com/s...

    B) Store the token somewhere - I believe a cookie is the best place. Check out this article: https://stormpath.com/blog/...

    C) On each future AJAX request, yes, you'll need to read the token and send it on a header.

    I hope this helps!

  • 2017-11-05 Ahmed Bhs

    I really need some help!! Any idea how to deal with authentication via ajax in front side ?
    I'm not sure maybe:
    1) send username+password json format ?no need to encrypt the json data before sending ?
    2) get the tocken from the api, and set it into local storage ?
    3) for every request do we need to send the token ?
    And what about the data base what should contain ?
    I will be so happy if you provide a jquery example.

  • 2017-01-09 alsbury

    +1 For refresh token tutorial

  • 2016-11-07 Ruslan

    Thanks Ryan. I've preferred to use the $request to store the data.

  • 2016-11-07 weaverryan

    Hey Ruslan!

    Yep, you can definitely do this :). With our setup, we didn't make any easy way, but effectively, you simply need to decode the JWT in the same way that we decode it in the authenticator. Basically, you can repeat the things we do here: https://knpuniversity.com/s... - grab the token from the header, and then decode it. Of course, it would be even better to take this code and put it into a new service class. Then, you could call that from your authenticator or from your controller. And alternative method would be to update your authenticator to "put" the decoded JWT "somewhere" - e.g. set it on a service... or even put it into the request object $request->attributes->set('jwt_data', $data) (where $data is the decoded JWT that we have in getUser() of the authenticator).

    Let me know if this helps! Cheers!

  • 2016-11-06 Ruslan

    Hi!

    Is there a correct way to get some data from jwt token in a controller? For example, if I add a company id to my token, how can I then get that id in my controller?

  • 2016-06-24 Vlad

    Thank you, Ryan! This totally makes sense.

  • 2016-06-24 weaverryan

    Hi Vlad!

    You're absolutely right. But their very nature, JWT's are not meant to expire - though you *could* make this happen, but storing them on your server and checking their status. There's actually a really good article about this: https://auth0.com/blog/2015...

    But, it does kind of defeat the purpose, since the beauty of JWT is that they are standalone and don't need to be stored or checked anywhere. But, you need to do what you need to do :).

    Cheers!

  • 2016-06-23 Vlad

    Hi Ryan and Victor,

    I've done some research on this. Correct me, if I'm wrong, but I believe, it is impossible to invalidate a token to force a logout.
    The token isn't stored anywhere on the server, but simply contains the information necessary for its validation, such as the username, expiration time.

    I haven't looked into OAuth, but I think, it might have something to accomplish this functionality.

    Thank you for your suggestions.

  • 2016-06-23 Vlad

    Hi Ryan,
    I want to also add some sort of a logout action to let the user logout, and not simply let the token expire.
    I was thinking of a route: /api/logout for that.
    How would I accomplish it? Is there a way to expire the token upon request?
    Thanks,

  • 2016-06-13 Vlad

    Thank you, Ryan!

  • 2016-06-13 Vlad

    Thank you, Ryan!

  • 2016-06-11 weaverryan

    Hey Vlad!

    This is less related to the firewall (which is mostly about "authentication" - identifying who you are) and more about authorization: denying access. We'll talk about this (next week actually) in the Symfony security tutorial: http://knpuniversity.com/sc....

    But basically, IS_AUTHENTICATED_ANONYMOUSLY is a way to guarantee that non-authenticated users have access to the regex path. For example, Obviously, we want users to be able to access /register and /login without needing to be logged in :).

    However, like routing, access controls match from top to bottom, and *stop* as soon as they match *one* line. This is important for the last 2 lines:

    1) ^/api if the URL has not matched any of the previous lines, then guarantee anonymous access to /api*. We do this just because we decided that we will deny access in the controller, not here.

    2) ^/ This matches *all* URLs. But, if the current URL already matched one of the above access controls, it will not reach this access control. This means that - for example - /login*, /register* and /api* will all be public (unless access is denied in the controller). But *every* other page will require you to be logged in.

    Cheers!

  • 2016-06-11 weaverryan

    Hi Vlad!

    Yes, JWT is sufficient for simple API authentication. And the short answer between JTW and OAuth2 is that the are different things: JWT is a type of token string (one that contains information). OAuth2 describes a process for *obtaining* a token. In fact, if you setup an OAuth2 server, you can actually issue JWT's as your access token :).

    OAuth2 is great - but it's also a bit complex if you're not an expert in it. I generally tell people not to worry about using OAuth2 for their app, unless they will have third-parties integrating with their API who will need to obtain access tokens for specific users (e.g. Facebook - many sites/apps need to obtain access tokens for different users on the site). This is the most common place you see OAuth.

    In case you haven't checked it out yet, we have an awesome tutorial explaining OAuth2 as well :) http://knpuniversity.com/sc...

    Cheers!

  • 2016-06-10 Vlad

    Hi Ryan,
    In security.yml, I noticed additional configuration:

    access_control:
    - { path: ^/_wdt|_profiler, roles: IS_AUTHENTICATED_ANONYMOUSLY }
    - { path: ^/register, roles: IS_AUTHENTICATED_ANONYMOUSLY }
    - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
    # allow anonymous API - if auth is needed, it's handled in the controller
    - { path: ^/api, roles: IS_AUTHENTICATED_ANONYMOUSLY }
    - { path: ^/, roles: IS_AUTHENTICATED_FULLY }

    Is this configuration related to the firewall?
    Thanks!

  • 2016-06-10 Vlad

    Hi Ryan,
    Thank you for the great tutorial.
    Is JWT Authentication sufficient for simple API authentication?
    What are the disadvantages of JWT over OAuth2?
    Thanks

  • 2016-06-09 weaverryan

    I think so too - I've added it to our idea list!

  • 2016-06-09 Roberto Briones Arg├╝elles

    I think that the topic of "Refresh token" is basic , It would be cool If you guys make a tutorial for this :D