What motivated me to write this
I was looking for good authentication tutorials when I was just starting with web development. Back then I didn't know any better. I came across a video on youtube and thought to myself, "WOW! It's that easy?"
Most of the videos I came across back then had a lot of flaws, some were using refresh token only to refresh access tokens automatically without doing anything else, while others were completely missing the point of refresh token.
This tutorial will not make you the master of authentication but after reading this, I'm hoping you'll understand the bare minimum that you need to implement a secure auth system for your web app. I'm also hoping to learn something new, and fix my own bad habits and mistake if you point it out.
Before we start
This is going to be a general guide and not a programming language specific tutorial. However, to show coding snippets and example, I will be using Typescript as it is a popular language among web devs.
In this article, we will take a look at basic auth flow, what tokens are and the use for a refresh token.
In the next article in this series, I will show code snippets and how to actually implement this.
The basic auth flow
When we visit a website and log into our account, we usually provide our email and password once. After that we can close the tab and even browser, but still are logged in. It'd be a hassle to log in every time we close the tab. This is why we use tokens. Once a user logs in, we send over a token from the server. After that for each request, the client/browser sends over the token and using that token we verify who the user is.
Generally when we log in, the server verifies that the email and password is correct. Then the server generates a token and send it to the client. This is how the authentication flow looks like assuming the log in was successful.
When the login is successful, the server sends a set of token (usually two) or session id as cookie. From there, every request will send that cookie to verify who the user is. The flow looks like this assuming the token is not expired and is correct.
Password Hashing
Repeat this with me, "Never store raw password in a database". Password is the key to our digital life and databases are not a secure home where no one can break in and steal that key.
To avoid our users password getting leaked, we use a one way encryption known as password hashing.
When user provides password to sign up, we hash the password and store it in our database.
When user provides password to sign in, we use the password hashing algorithm's verification function to check whether the provided password matches with the hashed password.
What is a token
In simple words, tokens in authentication are credentials that the server generates. To us, they might look like a randomized string.
It has a payload that contains information that is required for secure authorization. We use these information to authorize the user to different resources.
These tokens are cryptographically signed with a key. When the time comes for our server to verify the token, it verifies the signature to check if the token is valid and hasn't been tampered with.
Json Web Tokens or JWT are commonly used for authentication. We store payload, that is user id for example and we set a time when the token will be expired.
Why tokens should be short-lived
Unlike traditional session based authentication, tokens can be verified without storing each of them in the database.
If an attacker steals our token from a user, the attacker can pretend to be the user and use our services.
We could store compromised tokens in our database, but hitting the database each time a request arrives to check if this token is compromised defeats the whole purpose of using a JWT in the first place.
Short expiry time is a UX killer
If your favorite app asks you to log in every 15 minutes, how would you feel?
A shorter expiry time means our users need to log in every time the token is expired, to get a new one. To solve this issue, we can give our users two tokens, one will have aa expiry of 15 minutes and another will have a longer lifespan, like 14 days or even 30 days.
When the short-lived token, A.K.A the access token expires, we can generate a new pair of token and set them as cookie.
When we fresh the access token, we can store the token that was used to generate a new set somewhere (usually database). That way when a user tries to reuse an old token, we know that the token is stolen and we can immediately take action.
Because we use the 2nd token to refresh the token pair. It is called Refresh Token.
Refresh token
This is where most people mess up. The purpose of refresh tokens is to make sure our access token is short lived. That way if the tokens are stolen, we can minimize the damage.
In most tutorials I've seen people just generating new tokens with refresh token when access token is expired. Some check if the user still exists in the database, and others are confused and only generate new set of tokens without verifying anything.
That defeats the purpose.
Ideally, when a refresh token is used, we should check if this token was already used before to generate a new set of tokens.
Reuse of a used token is suspicious and it is better that we invalidate the entire token family as a security response.
We do not know who stole the token, but we know that the token is probably stolen. In that case, we should revoke the entire refresh token family.
We can mark each of our refresh tokens by a familyId or something else. This information should be stored in a database to identify tokens that belong to the same token family.
When a user logs in for the first time, we can create a familyId and attach it as payload to our refresh token.
When access token is expired and a new token is needed, we can extract and attach the same familyId to the new refresh token.
Of course a jti or token id should be stored in the database along with familyId in the database. The schema should at least contain these information,
userId - [id of the user]
familyId - [id that is attached to each refresh token]
usedAt - [timestamp when this refresh token was used, default NULL]
revokedAt - [when the refresh token was revoked, default NULL]
expiresAt - [when the token naturally expires]
jti - [primary key, unique for each refresh token]
When a token is compromised, we should invalidate all the tokens with same family id.
When user chooses an action to "Log out from everywhere", we should invalidate all the refresh tokens from our database containing the id of that user.
Conclusion
To put everything together.
When a user signs up for the very first time, we hash the password and store it. NEVER STORE PLAIN PASSWORDS TO DATABASE.
After successful login, we send two tokens to the user. Access Token and Refresh Token. Access tokens have shorter lifespan while refresh tokens have relatively longer lifespan.
When access token is expired, use refresh token to generate a new access token. Prevent refresh token reuse.
If we understand this much, we already have a good foundation to implement an authentication system.
Of course there are many security concerns like Cookie security, CSRF protection, XSS etc but we cannot cover all of them in one article.
The goal of the article is to understand why access tokens are short lived, the need for refresh tokens and a proper way to handle them together.
In the next part, we will take a look at how to implement this.
If you read it this far, thank you. Let me know what you think or if you know a better way this can be done.



Top comments (0)