Chapters
-
Course Code
Subscribe to download the code!Compatible PHP versions: >=5.5.9
Subscribe to download the code!Compatible PHP versions: >=5.5.9
-
This Video
Subscribe to download the video!
Subscribe to download the video!
-
Subtitles
Subscribe to download the subtitles!
Subscribe to download the subtitles!
-
Course Script
Subscribe to download the script!
Subscribe to download the script!
JWT: Other Things to Think about
Scroll down to the script below, click on any sentence (including terminal blocks) to jump to that spot in the video!
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeMostly, 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:
Show Lines
|
// ... lines 1 - 12 |
class TokenController extends BaseController | |
{ | |
Show Lines
|
// ... 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:
Show Lines
|
// ... lines 1 - 19 |
class JwtTokenAuthenticator extends AbstractGuardAuthenticator | |
{ | |
Show Lines
|
// ... 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]); | |
} | |
Show Lines
|
// ... 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:
Show Lines
|
// ... lines 1 - 72 |
lexik_jwt_authentication: | |
Show Lines
|
// ... 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.
34 Comments
+1 For refresh token tutorial
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 :)
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/JWTRefreshTokenBundle 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/JWTRefreshTokenBundle/blob/master/Entity/AbstractRefreshToken.php - 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/JWTRefreshTokenBundle 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!
Hello Ryan, Thanks for this good tutorial. About the Guard concept, it's a little sad to use JWT and to query back for auth. Is it possible to create a JwtUserProvider that only reads the payload of JWT ?
Hey Gompali!
Sorry for the slow reply on this one! Glad you enjoyed the tutorial - and it's an excellent question! So yes, I queried the database for the user... mostly because this is how many people will use JWT - many don't really take advantage of the decentralized potential, which is fine if you don't need it. To answer your question: definitely. Symfony's security system doesn't care at all if the user is stored in a database... or stored nowhere. So, the process would look like this:
1) Create a token whose payload contains all the data you need
2) In getUser()
of your authenticator, decode the data (like normal). But then, instead of using a key on it to query for a User object, simply instantiate your User class (which will be a normal class, not even an entity) and store whatever data you want on it from the pay.
That's it. Return that non-persisted User from getUser() and everything will work beautifully.
Does that answer your question and make sense? If I missed the point of your question, let me know :).
Cheers!
Yes I did what you described. I didn't check but I guess a session is opened. The problem is that i don't need a session. I just need my request to go through the firewall after the JWT is proved authentic. Then, the User is "just an id" for what I have to process.
By the way you mentioned the "stateless" option prevents Symfony from saving the User in the Session. What did you mean exactly ? User is found by security and loaded in Session but Session is cleared when the response fires ? Still useless to load a User IMHO.
Hey Gompali!
By the way you mentioned the "stateless" option prevents Symfony from saving the User in the Session. What did you mean exactly ? User is found by security and loaded in Session but Session is cleared when the response fires ?
If you set stateless: true
under your firewall, it means that the User object that's returned from your object is simply not stored in the session. It IS available like normal during that one request, but isn't stored in the session. That's all that means :).
Yes I did what you described. I didn't check but I guess a session is opened. The problem is that i don't need a session. I just need my request to go through the firewall after the JWT is proved authentic.
So, the process I described above works equally well with a "stateless" or "stateful" firewall. In both cases, your authenticator is called and its job is to return a User. That User is available as the "authenticated" user throughout the remainder of the request.
Then, the User is "just an id" for what I have to process
I think (? but I could be misunderstanding) that there is a small misunderstanding about the purpose of the User
object during authentication. In a high level, when you authenticate a request, Symfony wants some "object" that represents "the authenticated thing" (usually called User, but that doesn't really matter). This class doesn't need to be persisted to Doctrine and can look however you want.
Let's take an extreme example: you send a JWT that contains nothing more than a "user id". I would create a simple User
class (or maybe you have a better name for your class in your app) that only contains the id. The class does need to implement UserInterface, but I'll talk about that more in a moment:
namespace App\Security;
use Symfony\Component\Security\Core\User\UserInterface;
class User implements UserInterface
{
private $id;
public function __construct(int $id)
{
$this->id = $id;
}
public function getId(): int
{
return $this->id;
}
public function getUsername()
{
// not really used in core - it's a visual identifier
// of your User, and in core, is only used in the web debug toolbar
return $this->getId();
}
public function getRoles()
{
// return any "roles" you want
// probably you are not using this, if your JWT
// contains only an id, so just return a single
// role that all users will have
return ['ROLE_USER'];
}
public function getPassword()
{
// meaningless - will never be called
}
public function getSalt()
{
// meaningless - will never be called
}
public function eraseCredentials()
{
// meaningless - will never be called
}
}
The UserInterface - unfortunately - requires several methods that are simply not needed. That's something that needs to be fixed in Symfony (the interface should be smaller) - we all know it, it's difficult to do without breaking backwards compatibility ;). Anyways, you DO need to have these methods, but you can ignore almost all of them. What this gives you is basically a small wrapper around the "id" in your JWT. It's effectively a PHP class that represents your JWT data. This is what you'll return from your authenticator.
Let me know if that makes sense!
Cheers!
Yes yes. That's what I expected. Cristal clear. The Security component wants a User and with "stateless", user lives in memory and is not persisted anywhere. Thanks for clarifications.
Hi Guys,
I am using Symfony 3.3 and Lexik Authentication Bundle 1.x Branch configuration
I have implemented Lexik JWT token authentication and I want to implement Refresh token in the authentication bundle itself. Is it feasible? If yes please suggest
Hey Rajeshwari Nesargi!
I'm not sure about this, as I've never implemented it. But, in their docs, they point to this bundle: https://github.com/gesdinet... - I'm not sure if it's compat with 1.x of the Lexik bundle, but you can at least use it as a guide :).
Cheers!
I am unable to generate Refresh Token while generating Token. Tried several ways but didn't work out. Is there any guide or tutorial where I can refer to for symfony 3.3 & 1.x Authentication Bundle
Hey Rajeshwari Nesargi !
Sorry, not any that I'm aware of. It's just not something that's in the scope of this tutorial :/.
Good luck!
I am having troubles following the authentication flow. Actually I am working in a project where the front end is in React JS. Until now I have sent the parameters username and password and retrieve the message if it is logged in or not and works just fine with the LoginFormAuthenticator. I don't understand how manage the JwtToken. How it is placed to the user? How it is used in the front end?
Hey Majkell!
The best way to handle this depends on your app. Honestly, if your backend exists only for your React frontend, you don't need an JWT or any type of API token: you can just use a stateful firewall (in Symfony, that's the default, unless you explicitly say stateless: true
under your firewall), send an AJAX request to your login form... and be done! That response will contain the normal session cookie and all other AJAX requests will be authenticated..
If you DO want to create a truly, token-based API authentication, you, from JavaScript, would send a POST AJAX request up to /api/tokens (to follow this tutorial), sending the username and password as standard "HTTP auth". That endpoint would return a token that you can use on your frontend. You do need to be careful how you store that token in JavaScript - if tokens are improperly stored, they can be accessed by other JavaScript on your site.
Let me know if this helps! Cheers!
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
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!
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.
Hey ahmedbhs!
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!
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,
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.
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!
Thank you, Ryan! This totally makes sense.
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?
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/screencast/symfony-rest4/jwt-guard-authenticator#getcredentials - 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!
Thanks Ryan. I've preferred to use the $request to store the data.
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/JWTRefreshTokenBundle, 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 :)
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
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!
Thank you, Ryan!
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!
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!
Thank you, Ryan!
"Houston: no signs of life"
Start the conversation!
What PHP libraries does this tutorial use?
// composer.json
{
"require": {
"php": ">=5.5.9",
"symfony/symfony": "3.0.*", // v3.0.3
"doctrine/orm": "^2.5", // v2.5.4
"doctrine/doctrine-bundle": "^1.6", // 1.6.2
"doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
"symfony/swiftmailer-bundle": "^2.3", // v2.3.11
"symfony/monolog-bundle": "^2.8", // v2.10.0
"sensio/distribution-bundle": "^5.0", // v5.0.4
"sensio/framework-extra-bundle": "^3.0.2", // v3.0.14
"incenteev/composer-parameter-handler": "~2.0", // v2.1.2
"jms/serializer-bundle": "^1.1.0", // 1.1.0
"white-october/pagerfanta-bundle": "^1.0", // v1.0.5
"lexik/jwt-authentication-bundle": "^1.4" // v1.4.3
},
"require-dev": {
"sensio/generator-bundle": "^3.0", // v3.0.6
"symfony/phpunit-bridge": "^3.0", // v3.0.3
"behat/behat": "~3.1@dev", // dev-master
"behat/mink-extension": "~2.2.0", // v2.2
"behat/mink-goutte-driver": "~1.2.0", // v1.2.1
"behat/mink-selenium2-driver": "~1.3.0", // v1.3.1
"phpunit/phpunit": "~4.6.0", // 4.6.10
"doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
}
}
I think that the topic of "Refresh token" is basic , It would be cool If you guys make a tutorial for this :D