Refresh Token Rotation and Reuse Detection in Node.js JWT Authentication

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
today we're looking at refresh token rotation and how it can level up the jwt authentication and security of your rest api [Music] hello and welcome hi i'm dave we're going to add refresh token rotation and reuse detection to the node.js rest api that's built in my node.js for beginners course you could consider this tutorial an extra bonus chapter for that course and if you haven't completed that node.js beginners course you might consider doing that first it will really help you understand the starter code and what we're doing today i'll share links to all resources and source code in the description let's get started refresh token rotation is a change in strategy from the standard refresh token approach let's quickly review with jwt authentication an access token and a refresh token are issued after a successful login an access token lasts for a short amount of time think minutes here not hours and a refresh token lasts for a longer amount of time possibly one or more days the access token is sent back as json data and a front-end app should store it in memory but not in local storage or a cookie the refresh token is sent to the browser as an http only secure cookie it's not accessible by javascript but it does need to have an expiration set every time the api is accessed the access token is sent with the request when the access token expires the refresh token is used to request a new access token the refresh token is verified with its own endpoint and compared to data in the database it needs to expire or be removed during a manual logout but what if the refresh token was compromised malicious access would be granted until the refresh token expires and this brings us back to refresh token rotation with refresh token rotation every time a new access token is issued a new refresh token is also issued this doesn't eliminate the risk but it does greatly reduce it it's even more effective when combined with reuse detection this will allow a refresh token to be used only once after it is used it will be invalidated and if token reuse is attempted all refresh tokens for the user will be deleted which then requires the user to log in again as soon as their access token expires okay i've got visual studio code open here with the starter code and this is the full api that was built in my node.js for beginners course if you don't understand everything you see here i recommend you go there first okay looking at this code we're only going to change four files today we're going to implement refresh token rotation refresh token reuse detection and one other request i've had is to make this back end api support more than one device or more than one login at a time that's fairly easy to do so we'll add that as we go as well and we'll start in the model directory under the user model so here we're just going to change this refresh token data type from string to an array that holds string data and we can do that by wrapping this string declaration in an array and then we can save the model file and we're finished with that that's all we need to do now if you have existing users already in your mongodb store here for data you may want to change that because now we're going to start handling this refresh token data as an array when we change our code and so if i expand this you can see here i now have an array with a refresh token in there if you need to change it click the edit button there and you can of course scroll down to your data you can see i've got an array now that holds a string rather than just having a string and you can change this once you get into your mongodb okay i'm going to cancel that change and go back to visual studio code and with visual studio code now back open we're finished with the user model change and we can go to the controllers directory the other three files that will change today are all in here i'm going to double click on them to open each one and keep them open so there's the auth controller the logout controller and the refresh token controller now that i've got all of those open i'm going to press ctrl b in visual studio code to hide the file tree gives us a little more room to see our code now we just need to think our logic through we're making changes to the code and not writing it from scratch and so a lot of what we have will stay and the first thing is we receive our refresh token from that cookie that we send to the client which is the browser and here we say if there is no cookie we just send an unauthorized and we want to continue to do that and we also want to get the refresh token from the cookies.jwt if it does exist and then we get that data however now we want to delete the cookie after we receive it because we're going to be sending a new cookie so we need to do that we're already doing that in the log out controller so that's fairly easy to go to the log out controller copy the line of code that says clear cookie when we send that response just ctrl c to copy and go back to the refresh token controller and add a line underneath and just delete the cookie right there after we get the data out of it now the next line we have where we search for the user that has the refresh token we've received we need to keep that just the way it is and that's one very nice thing about using mongoose with mongodb we just use find one like we did with the string data even though it's an array and we're still searching for that refresh token within the refresh token array that we now have so we don't have to change that code at all this next line we are going to change because this says if we did not find a user but we did receive the fresh tote refresh token so that means that a refresh token does not exist anymore and it's already been used and deleted so this is now a reuse detection situation so here let's put detected refresh token reuse and i need to spell reuse correctly there we go so now we want to handle this code differently so instead of just the return with the 403 which is a forbidden response we want to put a little more code in here we want to decode the token that we received to see if we can pull out a username and match that to an account where we could delete all of the refresh tokens that exist with that account so that's going to be much like we evaluate the refresh token here when we have found one that we want to use so i'm just going to copy this starting at jwt dot verify go all the way down here to the error decoded function that starts and press control c so now inside of our if statement when we haven't found a user i'm going to put that same thing i'm going to close out the function and then also close out the jwt verify here on line 20. now this needs to be an async function at this point because we're going to access a database inside of here now we receive an error from this function if we cannot decode the refresh token essentially that means it's expired it's just no longer valid and so we don't need to worry about that so much so we can just say if we have an error then we can go ahead and return that same 403 that we would down here too so i'll just copy this same line and paste it right after that otherwise we know somebody is attempting to use a refresh token that would be valid if we hadn't invalidated it already because it was used before so with our refresh token rotation we're able to invalidate a token and that lets us know that okay this attempt is a reuse attempt and we don't want that so we'll call this hacked user now so we're defining a hacked user variable and we're just going to find the user here that has been hacked so we'll say await and then user find one and now inside of this we're going to look for the username that we get from decoded dot username we need to also remember to put the exec here so we can execute that call right at the end and then after we search for that hacked user hopefully we get the hacked user and now we're going to say refresh token and this is the data that we have in an array this is no longer a string we're just going to set it equal to an empty array so that deletes all the refresh tokens and there would be a refresh token in there for each device that the user has logged in so if we detect reuse we're going to make the user log in again as soon as their access token expires and we aren't in this tutorial however if you wanted to go back and put in all the code to actually save the access tokens to the database and check those against the database every time which would be a lot then you could actually empty that out as well and it would force an immediate login the next time they attempted to request anything from the api so it would be a lot more work a lot more data transactions but it would also log out that user just a little faster and again we won't be implementing that today but this is showing you how refresh token rotation works and that's much more common so here let's go ahead and say result and once we have a result we're going to save these changes so we'll say await hacked user and then call save and then we can go ahead and log this if we want to see what the result was as well okay and if those changes are saved we've essentially detected any kind of token reuse now up here above a reminder that if we don't get a cookie at all meaning if a request is sent to this endpoint that doesn't have a refresh token or a cookie at all then we send a 401 unauthorized so this code is only enacted essentially if we did get a cookie but we did not find a user that was linked to this refresh token inside the cookie and that means the refresh token has already been invalidated and by invalidated in our setup that means removed from that refresh token array in the database and if that refresh token has expired as well then we just send the 403 forbidden because we're going to get the error from this function that verifies the token however if it's still a valid token and someone is attempting to reuse it then we know we have an issue we find that username that we have saved in the refresh token previously and once we know what user to look up and invalidate their tokens we of course remove all the tokens and set that refresh token array to an empty array so that's what we have here now let's move on to where we have found the token it's a valid token and we're ready to reissue a new one but of course we still need to remove the old token from that refresh token array in the database so here we want to say const new refresh token array kind of a long name but it is very descriptive we'll say found user and then we'll call that refresh token array data and we can use array methods on this array that's in our database so here we'll say filter now for each refresh token and i'll just put rt for refresh token we're going to compare the refresh token and make sure it is not equal to the refresh token that we received which is essentially the old refresh token at this point so we're filtering out the old refresh token and creating a new array here without it okay i'll save that and now let's make some changes to the existing code we had when we had a successful or valid refresh token that was sent to us and we of course want to leave much of this the way it is we are going to access the database again with a weight so we need to make this an async function now inside of the jwt dot verify the rest of the beginning here stays the same but right after we start the function we want to do something else if we also have an error so we've received the token but at the same time the token has expired we found the user it was related to all of that is good but we have an expired token that is being replaced so at this point we need to update our data in the database we're going to say found user dot refresh token we're going to set that equal to and i'm just going to create a new array here and then spread in that new refresh token array and i'm in the habit of creating new arrays when i set data like this and much of that is from react as well but i just don't want to modify or change anything else so this is kind of a pure functional habit of mine if you're wondering why i didn't just set it equal to the new refresh token array and once we make that change of course we need to save it so we'll define result here and we'll set this to await found user and call save as well so that will update the database and now after that a 403 is still sent so we know okay at this point we found the user we had a refresh token but it was an old one and so now this is what would happen of course when that refresh token has expired but now at this point we can say the refresh token was still valid and we had a valid user to go with it everything was good there so now we're going to send back roles the access token which by the way we're putting the roles in the access token and this is something to note if you completed the react login series with me we also sent the roles here in the json by themselves and that was quick and easy to do and we didn't have to decode them but if you want you could eliminate sending the roles here just send the access token because we're putting the roles in the access token here but you would need like jwt decode or another library that's similar to decode that jwt on the front end if you were going to pull the rolls out and that might be a good way to do that okay back to the topic at hand we're going to create an access token but we're also going to create a refresh token here because a new one is going to be sent every time we send that new access token as well so we're already doing this in the auth controller let's jump over instead of recreating new code let's just take code that we already have in the auth controller so i'm copying everything here from line 26 to 33 and i'm going to press ctrl c to copy we'll go back to the refresh token controller and i'll paste this in but now let's change just because we already have refresh token defined so we need to turn this into new refresh token that we're creating here and that makes a little more sense it's descriptive and then we need to change how we save this as well because before we were just saving a string here so this found user dot refresh token is the data going back to the database once again so let's spread in our new refresh token array that we created and then let's also add in the new refresh token that we just created and now if we save that we're not quite finished yet because after that we want to send a cookie back as well and we were also doing that in the auth controller after we created that refresh token we can copy this line where we have a response.cookie and we're sending back an http only secure cookie as we see right here so ctrl c to copy that back to the refresh controller and right underneath here i'm going to paste in that code we just copied and saved to get a little better formatting but we also need to make a change here we're going to have a problem because we renamed this this is new refresh token now that we are sending inside the cookie there we go and everything else here should be good and we should now be finished with the refresh token controller so we are rotating the tokens we're deleting the old one we're sending a new one we're also detecting if there is any attempted reuse above and that's really what we wanted to handle and of course we're also working with multiple devices now because we have an array so we could have more than one refresh token it's going to be one refresh token per device that is rotated okay now that we're finished with the refresh token controller we're going to head over to the log out controller but first i just want to copy this code right here that says found user.refreshtoken.filter because we're going to need that so ctrl c to copy and heading over to the logout controller okay not many changes to make here at all as a matter of fact we're going to start out the same and we'll scroll down to where we have delete refresh token and database because once again we're not dealing with a string we're dealing with array data so instead of this string here we're going to set the database found user dot refresh token array equal to this filter and we'll just set it directly here we're only using it in this one spot so there's no need to create a variable we'll just filter the array right here which returns a new array and we can save this change and now let's go ahead and move over to the auth controller okay let's scroll to the top of the auth controller most of the changes will be below but one thing we want to do right at the beginning is define cookies here because there could be an existing cookie so we want to look at the request dot cookies and go ahead and grab it if it does exist there okay after that i'm going to press alt z just so some of the code that is scrolling off the screen can just wrap down to the next line and we don't need to make any other changes here until we get down to where we're creating this new refresh token that we're going to send so that is one thing we want to do right there let's select refresh token and we'll go ahead i don't want to select all instances of it because it's being used in different contexts in a few places here so we'll just say new refresh token i'll copy this and we also want to make sure that is what we send in the cookie so that will be the new refresh token that is being sent now let's create some room under where this new refresh token is created and i'll scroll up to give us a little bit more room as well and we want to say we're going to have a new array so let's say const new refresh token array and i'm going to go ahead and use a ternary statement here i know not everybody likes these but it's not going to be a chained ternary so you might not hate it too much this means if we don't have any cookies with the jwt so we're checking to see if we do not and if that is true on this line i'm going to just say found user dot refresh token and that means this new refresh token array is going to be equal to what was in the database already there were no cookies no old refresh token to delete out of the database essentially but if there is a cookie with a jwt then we want to remove it from the database so here we can say found user dot refresh token dot filter and once again we can take that refresh token each refresh token actually and compare it to what we're receiving from cookies.jwt which is essentially the old refresh token but we've never defined that above okay then also if we have a cookie an old cookie we want to remove it so now let's say if cookies dot jwt once again let's just jump over here and grab that clear cookie line that we have inside of the logout controller so i'm copying that with ctrl c jump back to the auth controller and we can put it right here so if we received a cookie at the auth controller we're going to go ahead and delete it now at this point now once again we need to change what we are saving into the database so let's change this found user.refresh token to this new array so we'll say new refresh token array is being spread in here and now we also need to include that new refresh token that was created with a successful login and after that we should be finished with the auth controller as well everything looks pretty good here so i believe it is time to fire this up in our dev environment and test it out i have visual studio code here on the left with our node rest api and on the right i have a react app with a login that will interact with our rest api so i'm going to press control and the backtick and then i'm going to type npm run dev to go ahead and start the rest api so we can interact with it and test those different endpoints and the first endpoint we're going to test is the logout endpoint which i'll of course have to log in for that first so i'm going to type in jane and i'll enter her password and hopefully the server will go ahead and start here in just a second and now it's up and running i'm going to press sign in and we'll see what we get back so we went ahead and hit the auth in point with post and you can see jane got assigned a refresh token right here so that's what we expect now because of the log statements that we have let's go ahead and check the log out where we after we save the user here with result and then await found user dot save and we log the result we'll know what goes to the database because that's the response is the result here so let's go ahead and look at the terminal again and see what response we get when we sign out we sign out and you can see jane's refresh token array is now an empty array so that's what we want for the logout the second possibility we want to test is having an old cookie an old token essentially in our browser when we go to the login so let's go to the auth controller and after we get the cookies i'm just going to paste in a console log statement here and this will say cookie available at login and we'll just use json stringify to look at that cookie in the terminal but that means we have a cookie when we attempt to log in it could have an old token though and that's what we want it to have so let's make sure we're going to let that token expire quickly let's see what we have here right now it says one day so let's change this to like 15 seconds which makes it just a little longer than the access token and after we log in we'll let that expire but then we'll go back to the login without logging out and that's how we could possibly still have a cookie because it wouldn't have been deleted by manually logging out so let's go to the login and oh yeah we want to go ahead and show the terminal again so we see that result so we'll bring that back up okay we're at the login we'll log jane back in she's logged in now right now she didn't have a cookie so you can see here in the terminal it says cookie available at login and it's an empty object but now she was assigned one and we see what refresh token was sent in that cookie as well so now we'll let this expire here over the 15 seconds that i'm talking and then we'll just go back to the login without signing out so we have a user that didn't sign out but they go back to the login page which is a possibility you could of course adjust your front-end app to let that not be the case but as a back-end developer we don't know what the front-end might do so we want to handle this case so now let's go to the login and we'll log jane in again and it will identify that she had an old token and there it is cookie available at auth and we can see the jwt cookie right here and we wanted to make sure that was handled that she was assigned a new refresh token as expected and she only has the one refresh token now so yes the old one was deleted and it identified that there was a new one so our code is working for that as well now we want to check our code for an expired refresh token and this would be in the refresh controller so let's make sure we have that code pulled up i'll hide the file tree once again and now this is the typical code that we would see or the typical action or process that we would see and we already had this so we just kind of want to make sure our code didn't change and make this process not work so what we have here is as we scroll down into the refresh token controller is where we know we have a good token and we're evaluating everything and we put in this new if statement so yes if we find an expired token then we're going to go ahead and update here i'll press alt z so we can see all the code we're going to update with the new refresh token array that took the old token out so we just want to make sure the old token is removed and we'll have this result here so let's do a couple of things i'm going to paste in a console log statement here that says yes we've identified an expired refresh token and then let's go ahead and console log that result as well so we know what the database gets after that expired refresh token is identified okay we'll pull the terminal back up it's restarting due to our changes okay looks like it's almost ready i'm going to sign out maybe the server hadn't started yet it's taking just a moment here and now the server is up and running will log in it might identify an old cookie if the log out wasn't in place when i signed out so let's see yes we had an old cookie okay that's going to be deleted so it was as if i didn't sign out because the server wasn't running when i clicked sign out but now we are logged in and we have a new refresh token let's let this token expire and it will take 15 seconds to do that and then it will identify that it is an expired token and handle it appropriately as well so now let's go ahead and go to the admin page it's probably been 15 seconds and it will identify we have an expired token and yes that's what we have here so we have expired refresh token it identified that it was expired and then our refresh token array is now emptied out so it deleted the old refresh token from the array and we were sent back to log in and that's what we want okay and while we're in the refresh token what we really want to check now is for refresh token detection or reuse detection actually so we need to scroll back up here where we were detecting that and let's go ahead and put that right before we save the hacked user here so i'll put in a console statement and we see attempted refresh token reuse so we will catch that in this process here and we are logging the result as well so we'll still see what goes to the database in okay we'll save this change and the server will need to restart once again and with the server restarted we're going to not only need to use our react app but we're going to use postman for this as well because we need two different sources to try to use the same token so first we'll log jane in here and actually i want to go back and make this refresh token last longer now i'm going to need longer than 15 seconds to make this happen so back in the auth controller when jane logs in let's switch this back to one day which would be much more normal operation here so now i'll expand this and we'll let the server restart again and then we'll get jane logged in the server is restarted i'm clicking sign in for jane and now she is logged in and we have a refresh token and we went straight to the admin page because that's where she was before that's fine i want to go ahead and take chrome and expand it to the full window right now and then i want to open dev tools i'm pressing ctrl shift and i to do that you could also right click and choose inspect but our dev tool should open we want to go to the application tab and then in the application tab you want to find the cookies and then we'll be on localhost 3000 and this is our secure cookie it's not available through javascript but one of these columns says secure i believe the one with s and the check mark here is a secure cookie but we can read this jwt the refresh token right here inside of devtools so we'll go ahead and grab this now jane hasn't used this yet but let's go ahead and have her use this refresh token and now we've got the users in the admin page she's got a new token you can see it changed now and so the token we had was different than this already let's go pull up postman now and attempt to reuse the previous token and there's postman and we can add a cookie notice i've already got the localhost 3500 and the refresh endpoint here we can add a cookie to postman over here on the right and i'm going to delete the existing one add a cookie and instead of cookie one equals value we'll put jwt equals and we'll paste in the refresh token that we had that jane already used we save this close this out and attempt to use the previously used refresh token i'll send it and we should get a forbidden response back in the response here and yes that is what we got from the api so that responds appropriately now this could also go the other way a hacker could somehow get that refresh token before jane uses it maybe inside that 15 minute window before the access token expires and at that point a hacker could actually get an access token for that limited time frame but of course they couldn't reuse the refresh token either so they would only have access before that access token expired so instead of the one day duration of the refresh token we've mitigated that down to possibly 15 minutes or less so it's a great improvement okay i'm going to close postman now and the final thing we want to check is just multi-device support i'm also going to close devtools and i can resize chrome back over here to the right side of the screen but what we want to check now is to see if we have multi-device support so let's sign jane out we hit the log out endpoint that's all good and now let's go log back in and get jane a new token here and now i'm going to open up firefox and do a second login you can see jane only has one refresh token right now but when i log into firefox it's actually simulating having another device essentially two browsers or two different devices either way i have the firefox browser off screen but i'm logging in jane right now and then we should see the response from the api and we do and now the refresh token array has two separate refresh tokens in here one for each device that is logged in or in this case each browser but that's what we expect and it will still rotate these tokens delete the old one that is on whichever browser appropriate or whichever device and then of course assign a new one so this does now have multi-device support as well and remember if you're interested in building your own react login like i have over here on the right i have a series about that and so i'll make sure to put a link to the react login tutorial series in the description below okay a last minute addition here sometimes it's good when i have an extra day or two to think about a tutorial and the code that i've created and there's no shame in refactoring code or adding code when you realize there's something you left out or something you need to change and that's exactly what i'm doing here we're in the auth controller and i thought of one other possible scenario and it would be kind of a fringe case but it could happen so we want to think about this let's start out on line 35 where we were defining the new refresh token array just use the let keyword here instead of the const keywords so we can reassign this if we want to the rest of this ternary statement stays the same but then after that we had an if statement and we were just saying if we had the jwt cookie then we were clearing the cookie but really we need to add reuse detection here in the auth controller and let me quickly go over this scenario so we have a user and the user logs in but never uses the refresh token and does not log out so that refresh token is not removed and then the refresh token is stolen and if one and two are true then we need to use reuse detection right here to clear all of the refresh tokens so when the user logs back in it would find that that was actually reused because the token wouldn't be there so that's what we're going to do here so we just grab the token from the cookies.jwt we search for that token and basically if we don't find the token we know it's already been used then because our user would not have used that token so it should still be in the array even if it's expired and then it would get detected in another way like we previously were however if they have not used their token but it's not in there then we know somebody else has used it so here we log attempted refresh token reuse at login and that lets us set this new refresh token array to empty after that everything else is still the same as it was before but i just wanted to add this last minute addition because i felt like it was important to the tutorial and the thought process overall remember to keep striving for progress over perfection and a little progress every day will go a very long way please give this video a like if it's helped you and thank you for watching and subscribing you're helping my channel grow have a great day and let's write more code together very soon
Info
Channel: Dave Gray
Views: 51,852
Rating: undefined out of 5
Keywords: node.js jwt authentication, node, node js, auth, authentication, refresh token rotation, refresh token, jwt refresh token, jwt token rotation, refresh token reuse detection, reuse detection, refresh token reuse, token reuse detection, jwt reuse detection, jwt, jwt auth, jwt token, nodejs, nodejs jwt, jwt authentication, node.js authentication, node.js auth, rotate refresh tokens, access token, rest api, node rest api, express rest api, express, js, web token rotation, web token
Id: s-4k5TcGKHg
Channel Id: undefined
Length: 35min 7sec (2107 seconds)
Published: Fri Mar 11 2022
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.