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!

  • 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/03...

    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/scree....

    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/scree...

    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