ASP.NET and JWT Refresh Tokens

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
Over the past several videos we've been looking  at using JSON Web Tokens for authentication and   authorization. And a very closely associated  concept that lots of people have been asking me   to go into is the idea of refresh tokens. Because  the thing about the JWT is that once you've got   hold of it - if somebody has managed to steal it,  for example - it entirely of itself can give you   access to the web service. And the only limitation  on that is that we have that expiration time that   in the examples I've been giving you, I'd set  to three hours. So if somebody does steal it,   they've still got a limit on the amount of time  they've got access for. And so the shorter we   make that expiration time, the more secure we're  being because we'll risk fewer of those problems.   The disadvantage to that, however, is if we  have a shorter expiration time, then when the   Web Token does expire, you've got to re-enter your  credentials - your username and password - to get   a new JWT. And so that's inconvenient. So we've  got this compromise between security by making the   expiration time short and convenience by making it  longer. And the refresh token is a way to help us   with that compromise. Because what we do, as well  as issuing the JWT - the access token - we issue   this thing called a refresh token, which has a  longer expiry than the JWT. And therefore when the   JWT does expire, rather than having to enter our  full credentials to get a new one, we can offer   up the refresh token. And that will give us back  a new JWT, if everything's working okay. Now that   may sound like no difference at all; why not just  make the JWT have the longer expiry rather than   having this separate refresh token? But it does  give us some advantages. It's not perfect, but it   makes that compromise better. The first advantage  is there are two of them. So even if somebody had   stolen your JWT, they haven't got hold of your  refresh token or vice versa. And you need both   of them to do the refresh. So when you send off  the refresh token, you've also got to have an   expired JWT. Otherwise, both of them won't work  without the other. And then the other advantage   you've got - which is very different from a JWT  - a refresh token can be revoked. So if you know   your system is compromised, or simply if you've  logged out, you can revoke the refresh token.   And therefore even if somebody has it, it'll be  rejected because we know they're not allowed to   use it. So those are the two big advantages. We'll  see a few more as we look at the details of it. So   let's dive in and look at the technology of this.  I'm going to do this video on what's happening on   server side and then the next video, I'll show you  what's happening client side. I'm just going to do   this in Blazor because that seems to be the most  popular one that I've done. But I'm sure we can   work out how to do it in Angular and in React.  So let's take a look what we've got, and I've   just made a few changes to the existing server  simply really, I've just put in some logging. So   if we look at the AuthenticationController, you  can see I'm just putting a log when we register   and if we're successful, and a log when we log in  and that sort of thing. And then also I've put in   on the Program here, I've put in some logging for  the actual authentication. The way we can do this,   you can see down here where had the AddJwtBearer,  and I've got the options in there. The new bit   I've done is on these two events, the OnChallenge  - so that's basically if the access fails, you   haven't got a valid JWT - and the OnTokenValidated  - that's what happens when it does. And then you   can see I've just called this LogAttempt and it  just gives us a bit of information. Particularly   what it gives us is the current time on the server  and the expiration of the token. So I'm just   going to do one more thing here, which is actually  change the timeout to something rather shorter. So   when we do the login, previously, we had in this  GenerateJwt, we had add three hours. But now I'm   just going to change that to adding 30 seconds, so  if we go to ‘AddSeconds’ and make that 30 seconds,   just so we're not waiting around to see  the expiry. And so if we run that up,   there we can see the Swagger that we've got. And  also if we just take a quick look at the console,   this is where we'll see those logging messages  coming in. I've turned off the other messages just   so it's not too messy. And so what we can do here  is if I, for example, try to just get the book   reviews, then we can see we got that OnChallenge.  And there's no JWT present at all, because that's   not working. But if I were to do my login and use  the same old login that we've been using for ages,   then we can see it's ‘Login called’ - takes  a second or two for the first time … ‘Login   succeeded’. And we've got back our JWT  and as before, I can now pop that into the   Authorize up here. And that will now be sent  off in the header with every request. And so   now if I try to get the data, we can see that  we've got the token validated we can see the   current time as 9:23:36 and expiration  time - not long – 9:23:48. And if we   just pop that back and execute again, we  can see that it's allowed us to come in   there again. But actually something rather odd  has happened here. Because while I was talking,   we actually went beyond the expiration. So the  system time is 9:23:58, but the expiration was   9:23:48. And the reason for this is because of a  concept known as Clock Skew. What you need to bear   in mind with this is what I've done, it's a little  bit unusual in that I've got a single server;   that is both my authentication server, giving  the login and that sort of thing and also is my   data server giving me the book reviews. And more  commonly these would be on different machines;   you'd have possibly a third-party organisation  that's doing your authentication for you,   and they will give you the JWT that then works  on your site. And we can think of that in terms   of anything with this AuthenticationController  could be on a different server from the book   reviews. But once you've got that, then you  get the possibility that the system clocks on   the authentication server and your server are not  in sync. And so, particularly if you've got short   expiry times, you've got to have a bit of leeway  on the timeout comparing the expiry time to the   current time, and that's the thing called Clock  Skew. And the default that you get with ASP.NET   is a rather large five minutes. And so it's  advisable to shorten that to some extent. Again,   exactly how short you make it, you've got to make  decisions on that, but I'm going to shorten it   right down to five seconds. So if we go into  the Program, and then here in the options,   when we have the TokenValidationParameters on  to that, we can add this ‘ClockSkew’ and change   that to just five seconds. So ‘new TimeSpan(0, 0,  5)’. And that might be a bit short, but we want   to just get things moving along quickly. And so now  if we run that up, and once again, let's log in   and execute that and get hold  of the token, pop it in here.   And then let's start accessing the data.   Then we can see we've got a validated there. And  in that case, we're within the time limit just.   But now, if we try that, again, we can see we've  got the OnChallenge, which means it's failed.   And we can see we get into the 401 Not Authorised,  and the system time is more than five seconds   beyond the expiration time. So we've got a very  tight time there. And that's going to get very   irritating if we have to keep logging on every  30 seconds. Not realistic, but whatever the time   may be, we want to have these refresh tokens  to make life easier. So let's put that in. So   let's go back to the code and stop that. And the  first thing to notice about a refresh token that   makes it different from a JWT is it does actually  require extra storage in the database. So a JWT,   remember, once it's been sent to the client, the  client sends it back to the server, the server   verifies it simply by using the encryption on  the JWT. That's the only way it knows that it's   correct. It doesn't need to make any reference  to a database, which is something that also makes   that process faster. But with a refresh token, we  do need to put it in a database so that it can be   revoked, as we discussed. So let's do that. Let's  go to our LibraryUser. So remember, this is the   data type derived from IdentityUser that actually  puts data in the database. And what we're going to   add to that is a couple of things, we're going to  have a property of type nullable string - so it'll   be a nullable column in the database - and we'll  just call that ‘RefreshToken’. So that's where the   token goes. And then also stored in the database,  we're going to have a property of type ‘DateTime’,   which is going to be the ‘RefreshTokenExpiry’. So  unlike the JWT, the expiry is not stored embedded   in the token; the token is just a unique ID and  the expiry is looked up in the database. So that's   what we've got there, then we've got to do the  work of adding that to the database. So simply   enough, we just do our ‘add-migration’. And we'll  call that ‘AddRefreshToken’. So we'll let that   run and then we'll do an ‘update-database’ and  that will put that into the database. Not going   to bother to check that; believe me, that's in there.  Now we've got to make use of that. So what we've   now got to do is in the AuthenticationController,  we've got to issue a refresh token as well as a   JWT. So let's do that. So the first thing I'm  going to do is just pop into here a ‘private   static’ method that's going to return  a ‘string’. And it's going to be called   ‘GenerateRefreshToken’. And all the token is  going to be is a random number, effectively.   So I'm going to say ‘var randomNumber’. And we're  going to have this initially as an array of bytes,   so ‘new byte[64]’. So that should be big enough.  And then I'm going to use not the regular random   number generator - just Random that you get in  the System namespace - because although that's   good enough for certain situations, like gaming  and that sort of thing maybe, it's not regarded   as secure. Those random numbers are somewhat  predictable. Now, it'd be very difficult to predict   them, but it's regarded as a risk. So there is a  cryptographic random number generator that is much   better at this and much more secure. So we're  going to say ‘generator =’ and then we've got   this ‘RandomNumberGenerator’ from ‘Cryptography’  and that is simply used as a factory to create   that. And then once we got those two, then we  can say, ‘generator.GetBytes’ and then we just   put that ‘randomNumber’ into there. And that  will populate it with just this random number.   And then finally, we want that as a string.  So we ‘return’. And we'll do a ‘Convert’   and then we just want to turn this into base  64, as we've seen before. So that's our random   number turned into a string. And that's going  to be unique every time. Then we're going to   make use of that, so now we need to make sure  that that gets returned with the other bits and   pieces when we do the login. And so remember,  the login returns this LoginResponse. So let's   alter that so that it's also got the refresh  token. So we'll put in here, just a ‘prop’.   It's going to be a ‘required string’ and  we'll call this ‘RefreshToken’. I might   for the convenience for the end user also put  another one in for the refresh token expiration,   just like we've got for the JWT expiration,  but that just complicates matters. Sometimes   you want to do that, but I'm going to leave it  out. Then back in our AuthenticationController,   you can see we're getting an error there. So now  what we've got to do is in our Login, we've got to   use that method I just wrote. So we're going to  say ‘var refreshToken =’ and then just use that   ‘GenerateRefreshToken()’. So that'll give it us  back. We then got to do a couple of things with   it. We've got to, remember, store this in the  database. And we've just added onto our 'user'   object, we had the ‘RefreshToken’ - that's what I  just put in there. And then also, we've got to on   our user object, set the ‘RefreshTokenExpiry’.  And again, we can choose whatever we like. The   basic idea is it should be longer than the JWT  expiry. What I'm going to do is simply set that to   one minute. That's a ridiculously short time for  real, but it's greater than our 30 seconds, it'll just   mean we can demo this more easily. So we'll have  ‘UtcNow.AddMinutes’ and just add the one minute   onto there. And then we've got to store that in  the database. So we can do that with an ‘await’   and then we've already got our ‘_userManager’ and  we just do an ‘UpdateAsync’ and then pass in the   user that we've altered, so that will be stored  in the database. And then finally, we've got to   pop that into the LoginResponse. So if in here,  we just do the ‘RefreshToken = refreshToken’ and   that's that all done. Let's just run that up, see  what's happening there. So if we now do our Login,   and execute that, then we can see we're getting  back that refresh token. So just the base 64   encoded version of what we had. And if we take  a quick look in the database, then here in   AuthenticatedLibrary, look at the tables. If we  look in the User table, and scroll over, there   we can see on the end, there's the RefreshToken.  And there's the expiry that we gave it. And it's   probably already out of date, because we only gave  it a minute, but we can see that that's working.   Okay, now we need to start making use of that.  And we've got to provide another endpoint on the   AuthenticationController that will actually accept  this refresh token and give us back the new JWT.   So let's do that next. So what we're going to  do is back in here, we're going to put in this   new endpoint. And we're going to make this a  POST for similar reasons to the way we made   Login a POST. So it's another way of logging in,  really. And so we're going to call this ‘Refresh’.   I'm going to give it a few response types just  so that Swagger can give us a nice view of what's   going on. So it's going to have ‘Unauthorised’,  it's going to have an ‘InternalServerError’ and   it's also going to have obviously enough ‘OK’ if  it succeeds. So that's what we've got there. And   then it's going to be a ‘public async’ it's going  to be a ‘Task’ returning an ‘IActionResult’ and   it's going to be called ‘Refresh’. And the data  that comes in is going to be coming in through the   body, because obviously, we don't want to send the  refresh token in the URI, because that's not going   to be secure. So it needs to come from the body.  And in fact, we need a new type for this. So let's   go back to Authentication and add in here, a new  class that we're going to call ‘RefreshModel’.   And the ‘RefreshModel’ is just going to take a  couple of things. As we said, you need two things   to do refresh: you need the refresh token, but  you also need the JWT. So we're going to have a   ‘required string’ and I'll call this ‘AccessToken’  - that's the JWT, just to distinguish it from the   refresh token. And then we'll have another  one of those that we’ll call RefreshToken’.   And so that's the data that's going to be sent  in when we do a Refresh. Back in the controller,   that's going to be ‘[FromBody]’ and then  ‘RefreshModel model’. So that's our data   coming in. Just so we can see what's going  on, let's log that. So we'll just put in here   ‘LogInformation’ and then simply ‘Refresh called’.  And then what we need to do is find out who it   is that's trying to do the refresh. Now you'll  notice we have not put an [Authorize] on this,   because this is for someone who is not authorised  because their JWT has expired. So we've got to get   who it is and we get that from the JWT that's  passed in. So we're going to have to write a   method to do this. And so what I'm going to do  is we'll have a ‘private’ method that returns   this idea of a ‘ClaimsPrincipal’ - or not,  so make that optional. And then we'll call   this ‘GetPrincipalFromExpiredToken’ and pass  in the ‘token’. And then we need to create   some ‘TokenValidationParameters’ so that we can  have a look at the internals of this token. And   we do that basically by copying the ones that  we had in the Program. So here, we set up these   ‘TokenValidationParameters’. So we'll start out  by copying those and will just in here, say ‘var   validation =’ all of that. Or at least that's  a good starting point - need to change a few   things. One thing is our configuration is no  longer coming from the builder, we've already   got a ‘_configuration’ injected in there, so we  can put that in there. And in there, we also need   to get hold of our secret. So that again, we had  here in the Program, again, reading it from the   configuration. So we'll put that in there. And  we've got the possible error if that goes wrong.   But things we don't need - the one thing we don't  need here that's really important - is we don't   want to have the lifetime validated, because the  whole point is this has expired. So we don't need   the ClockSkew, because it's not even going to  look at that. We just can say ‘ValidateLifetime’   and then that ‘= false’. Okay, so what that  means is, it's still going to do the check   on the security with the encrypted bit, but it's  not going to reject this on the basis of it being   expired. It will reject it if anything else is  wrong. And so then we need to use that to generate   the actual token. So all we now do is we say  ‘return’ and then ‘new JwtSecurityTokenHandler’.   So this is what's been used internally when we  were logging in previously, but now we're doing   it explicitly and we use that to validate  the token. And then we pass in the ‘token’   and the ‘validation’ parameters that we've just  created. And then also, that returns the actual   security token - so the security token that would  have been decomposed from the string version we   put in. But actually, we're not interested  in that, so I’ll just put a discard on that,   because we don't need it. So that's now got all of that working. So that's what we can have and we   can make use of it up here. So we're going to say  ‘var principal =’ and then we can have just this   ‘GetPrincipalFromExpiredToken’ and we can pass in  ‘model.AccessToken’ and that should have got in   there. Now we need to check that that's actually  worked - that has given us a valid token. So what   we're going to do is say ‘if’ and then firstly,  check whether we had a ‘principal’ at all,   because that could come back null. Then we're  going to see if that has an ‘Identity’ and then   we're going to see if the ‘Identity’ has a ‘Name’.  And then we're going to test whether the ‘Name is   null’. So if any of those have failed, then that  whole thing will come out as null. And in that   case, we're simply going to bail out, so we're  going to do a ‘return Unauthorized()’ because   something's gone wrong. And as ever, you don't  really want to tell your end user anything about   what's gone wrong, because they may be trying to  somehow hack your system. So having done that,   we then need to get hold of the actual user  object. So we can say ‘var user’ and again,   we can get this from the ‘_userManager’ and we  can just do a ‘FindByNameAsync’ and we've already   got that name because we just checked that it was  there. And let's not forget to ‘await’ that one.   And then want to check, firstly, that we've  got a ‘user’ at all because it could be that   that's an invalid user still. So ‘if’ the  ‘user is null’. Then we also want to check   that the user's refresh token is the same  one as the one that we've just had passed   in. So ‘model.RefreshToken’, because those  have to match - that's what we're checking,   to see this is the correct refresh token. And  then finally, we need to check that the refresh   token hasn't expired. So we need to say ‘||  user.RefreshTokenExpiry < DateTime.UtcNow’.   So three things we're checking. We’re checking  we've got a user at all, we're checking that that   user matches the refresh token that's being handed  and we're checking that the refresh token hasn't   expired. Notice again, though, we're not getting  the expiry from the token like we do with the JWT,   we're getting the expiry from the database  where we set it up when we logged in. So if   any of those fails, again, we're going to ‘return  Unauthorized()’. But if it works, we're now in the   position to give them back the JWT. So we can  say ‘var’ and then ‘token =’. We've got this   function that we'd already used in the regular  login, this ‘GenerateJwt’ and we pass into that   simply the username, which again we saw it was  ‘principal.Identity.Name’. So there's our token.   And then the last thing we have to do, we’ll  again just log that so that we know that's worked.   So if it fails, we'll just see the ‘Refresh  called’ and nothing else, because it'll leave   on those returns. But if it succeeded, we'll  get this far. And then all we have to do is   basically the same that we did with the Login. So  if we just grab that, put that in there, change   that to ‘model.RefreshToken’ because we're just  giving them back the same refresh token. Now you   don't always do it like this. Sometimes you'll see  situations where if you do a Refresh, you refresh   the refresh token as well as the JWT. So you give  yourself a bit more time on the refresh token.   Whether that's secure or not, it's up to you to  decide. I've decided to just stick with the   same one, really so we can see how it eventually  expires. But you could do it by generating a new   one with a new time, but we haven't gone for  that. Okay, so that's what we've got there. And   that really gives us the basic functionality  that we want. So if we now run that up …   Again, we'll put the two of these side  by side so we can see what's going on.   And just to prove it's not working to start with,  so let's open that and do a try it out. And so the   JWT is not present. Now let's go for a Login. And  so I'll try that one out, normal sort of thing,   ‘Jasper Kent’ and ‘Pa$$w0rd’. Execute that. ‘Login  called’, ‘Login succeeded’ and so we've got back   our JWT. So let's make use of that. Got to be  fairly quick here, because we haven't got long. So   we put in ‘Bearer’ and then paste that in there.  ‘Authorize’, close that and then back down to the   get. And you can see that was fine. So our data's  come back, because we were within the time limit   of 30 seconds. If we hit that again, we're still  there, though we're getting quite close. Give it   a little bit longer, because remember, we’ve got  the five second Clock Skew as well still there.   Now it's expired. So now we've got to do  the Refresh. So if I go to the Refresh,   and try that out. Go to be fairly quick here. So  the access token I'd already copied, but then I   also need to get hold of the refresh token. So  that's what we had there. Pass that in there,   execute that we've got the refresh succeeded.  And so it's given us back a new JWT. So now if   I grab that one, and put it into the Authorize  - so log out with the old one, put in the new one,   authorize that. And then we should find back  down here that our execute is working again,   but only for another 30 seconds. So I had to  do that pretty fast, just because I've set   those limits so short. But you can see in a bigger  system, if, say, your JWT was set for three hours,   and maybe your refresh token was set for 24 hours,  that would mean that you could get back in without   having to re-enter your credentials, which is  probably going to be a safer way to go. There's one   last thing I want to add on here though, because  remember we said one very important thing about a   refresh token is it can be revoked, unlike a JWT.  So let's pop that one in there as well. So what I   want to do is on the AuthenticationController,  I'm going to add a new endpoint for revoke. So   let's just steal a little bit of that to make  life simpler to start with, and paste that in   there. And so first thing is, this is not going  to be a POST; this is going to be a DELETE,   because we're actually going to be deleting the  refresh token from the database. So ‘HttpDelete’,   we'll call it ‘Revoke’. Also, I'm going to make  this one authorized. So this is something that   will be a bit different if we have a separate  authentication server from the data server,   because you wouldn't actually be logged into the  authentication server typically. But we are logged   in here, so we can just use ‘[Authorize]’. So  you're only allowed to do the revoke if you're   logged in, and you're a valid user. So we've got  that. The status codes are going to be the same.   We'll change that to ‘Revoke’ as well. And then  there are going to be no parameters, because we're   simply going to revoke for the currently logged  on user. So that's what we've got there. Again,   we'll start with just a quick log message so we  can see where we are. So ‘LogInformation’ and   we'll just put on there ‘Revoke called’. And then  the first thing we need to do is get hold of the   identity of the logged in user. And that's easy  enough, we can say ‘var username =’ and then it's   available on the ‘HttpContext’. So if we get hold  of that, and look at ‘User.Identity’. Identity   may be null so we’ll just put a ‘?’ in there and  then get hold of the ‘Name’. So that will give us   the name of the logged in user, if there is one.  That's the kind-of odd thing here. Because we're   authorized, we know the must be one. But as far  as C# is concerned, Identity still could be null.   So we've got to put that in there. And then we  can put a check for that anyway, so just put ‘if   (username is null)’ then we're going to ‘return  Unauthorized()’. And then we're going to from the   username get a hold of the user. So we can say  ‘var user =’ and we can say ‘await _userManager’   and then ‘FindByNameAsync’. And that's going  to take the username that we just got hold of.   And then having got hold of the user, again we're  going to check we’re there. So ‘if (user is null)   and then another ‘return Unauthorized()’.  And then having got to that point,   all we now need to do is remove the refresh  token. So we say ‘user.RefreshToken = null;’   And then we just need to update that, so another  ‘await’ and then on the ‘_userManager’ we can say   ‘UpdateAsync’ and pass in the ‘user’. We don't  need to do anything with the RefreshTokenExpiry,   because if we haven't got a RefreshToken,  it's never going to look at that. So we can   just leave it like that. So having got that  far, we'll do a another quick log, and just   say ‘… succeeded’. And then the only thing we need  to return is just ‘Ok()’ to say that's worked - no   data really to go back. So that's our Revoke  put together. And so now if we run that up …   And again, let's compare both the console and the  Swagger. And so first thing we'll do is log in.   So our usual way of doing this … and ‘Pa$$w0rd’.   And so we'll execute that. We'll just see the  message ‘Login called’ and ‘Login succeeded’.   That gives us back our JWT. Let's take that  and use that for authorization. So ‘Bearer’   and then paste that in. And we should now see that  we've got access to the data there. So if we just   look at that, we're getting the data coming back.  But now let's call Revoke. So all we need to do is   go to the Revoke that we've got here that DELETE.  Click ‘Try it out’. Nothing particular to do,   just execute that. We can see ‘Revoke  called’ and ‘Revoke succeeded’. Now,   if we tried to do a Refresh, so let's go in  there and try that out. I've already got the JWT,   so that one goes in there. But if I scroll back  up to the Login and get hold of the refresh token,   and then paste that in there, then we can see  that we get back a 401 because we no longer had   a refresh token in the database. And therefore, it  couldn't match anything against the one we passed   in. And if we just take a look at the database,  so just go into the SQL Server Object Explorer,   have a look in there - just do a quick refresh.  And we can see over here on the right,   it's still got the old expiry date hanging around,  but that doesn't matter. It's the fact that we've   got NULL in there is why it wasn't working.  So that was the things we need to do on the   server side to start using refresh tokens. So we  needed to make sure we've got extra columns in   the database. We've got to generate the refresh  token and its expiry date when we do a login.   Store them in the database. Hand back the refresh  token. And then we needed a Refresh endpoint that   accepted both the JWT and the refresh token and,  if everything was in order, gave us a new JWT,   optionally a new refresh token, but we didn't do  that. And then also you need a Revoke so that you   can remove that. And that means we've now got  an explicit way of logging out. So previously,   we logged out just by throwing away the JWT. But  obviously, if someone had acquired it, they could   still use it. Whereas now we've explicitly  logged out, they may be able to use the JWT,   but they won't be able to use the refresh token  because it's now invalid. Next time, we're going   to see how that can all come together, because  it's quite fiddly doing all that in Swagger. So   we're going to add the functionality to a Blazor  application and we can see how all that works. So  I hope enjoyed that. If you did, do subscribe,  do click ‘Like’ and I'll see you next time.
Info
Channel: Coding Tutorials
Views: 1,019
Rating: undefined out of 5
Keywords: ASP.NET, JSON Web Tokens, JWTs, Refresh Tokens, ClockSkew, Captioned
Id: xfXX9Gu_cpE
Channel Id: undefined
Length: 30min 43sec (1843 seconds)
Published: Fri Sep 01 2023
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.