Ultimate Guide to Securing Your Application with JWT: Best Practices and Tips
JWT (JSON Web Token) is one of the most popular token-based authentications. JWT doesn't store any session on the server side, it makes the scaling process easier since each node server could validate the token without sharing the session. JWT solves authentication issues when you have multiple nodes for your application.
However, the JWT could be a problem if we can't implement it properly. Some of the vulnerabilities of JWT are stolen tokens, token tampering, and token expiration issues.
Here are some of my tips for you to make your JWT more secure:
- Keep sensitive data separate: Avoid storing important information in the token.
JWT is composed of 3 parts: header, payload, and signature. Payload is the place where you put your data, it's encoded using the Base64 algorithm. Every people could decode the payload, that's why you should not put sensitive data inside your token payloads. - Use short expiration times: Set tokens to expire quickly for better security.
Longer token expiration is more vulnerable since the hacker has more time to steal your token and use it. - Implement Token Revocation: When the user log-out from the system, immediately block their tokens and regularly clean up the old ones.
We can't invalidate the JWT, so when the user logout, we just remove the token but it still remains alive. It's not good and someone may use the token. That's why we should revoke the token, black list technique is one of them. We store the logout token in the database and check them whenever we validate tokens. - Secure token storage on the front end: Use secure cookie settings like HTTP Only, SameSite, and expiration configurations.
Make sure we keep the token in secure way in the front end to prevent the token from being stolen. Another trick is to use a unique cookie name to prevent people from guessing the token cookie names. - Always verify token signatures: It makes sure that no one modified the token.
Token signature is a hash string generated using the header + payload of the token. The header is composed of the algorithm and secret key that is used to generate the tokens. If someone changes the payload, the signature will no longer be valid. It makes the JWT has strong integrity. - Renew tokens before they expire: Refresh tokens proactively to ensure a smooth user experience on the front end.
Those tips are enough to make your app secure. But theoretical things always seem to be easier than real things. Let me show you how to implement them in real applications.
Secure Flask — NextJs Application Using JWT.
Here we gonna implement the JWT best practice using Flask and NextJs. We gonna create the Restful APIs using Flask, one of the most simple yet powerful frameworks.
And for the client side, we gonna use Next Js. NextJs is a framework built on top of ReactJs. The best thing about next is, that you don't have to install the babel, webpack and etc. It’s already provided.
On the database, we gonna use PostgreSQL.
Before we go deep, you may need to install the following prerequisite tech:
- Python (Current use version: 3.11)
- NodeJs (Current use version: 18.16)
- PostgreSQL (Current use version: 15.1)
- PgAdmin (Current use version: PgAdmin4 v6.5)
- Postman
Prepare The Databases.
Let’s talk about the database we use. In the database, we need to create several tables. For the authentication mechanism, we need to create tables for user, role, permission, role_permission, and revoked token. For the data sample, we need to create product tables.
This is what the ERD looks like:
To create the database dan tables, run the following queries.
The queries will also create some dummy data for us.
Open your databases using PgAdmin. You will have a database named ‘jwt_best_practices’.
If you open the detail of tables, you will see the list of created tables.
Setup The Project.
Create a folder on your computer.
Create 3 folders inside it.
‘api’ will be the back-end service which is the restful API.
‘ui’ will be the front end that we gonna build using NextJs.
‘cron’ is a scheduler/cron job that we gonna use for automatic tasks, I will explain it later.
Creating Restful APIs With Flask.
Inside the ‘api’ folders, create the following file.
- requirements.txt
Now let's create a virtual environment and install all the dependencies.
Run the following command.
python -m venv env
.\env\Scripts\activate
pip install -r requirements.txt
It will install all the dependencies.
Create Login Service.
First, we gonna create the login service.
Create the following files.
- config.py
- main.py
Create 2 folders inside the ‘api’ folder, and name them model and controller.
Create the following files.
- model > db.py
- model > user.py
- model > role_permission.py
- controller > role_permission.py
- controller > token.py
- controller > user.py
This is what the project looks like now:
Set the project environment by running the following commands.
SET DB_NAME=jwt_best_practices
SET DB_USER=postgres
SET DB_PASSWORD=root
SET DB_HOST=localhost
SET DB_PORT=5432
SET ACCESS_TOKEN_SECRET_KEY=a447ca5dc8ad11ed9d53dd69bd92e9d8
SET REFRESH_TOKEN_SECRET_KEY=abc1963fc8b911ed9fcedd69bd92e9d8
To run the app, execute the following command.
python main.py
Use Postman to check the APIs.
Let’s try to log in.
On the login process, we use basic auth to send the username and password.
As you can see, if the login success, we will receive access and refresh tokens. If it’s failed, it will return an error message.
Now copy the access token and paste it into jwt.io. It’s an official debugger to inspect your token.
It will show you what is inside your tokens and what algorithm is used. Here you could also modify the payloads. That’s why we should not put sensitive data inside the token payload.
Copy the role_id, and use it to get the permissions.
Now we know what are the user capable to do.
Let's create the product APIs.
Create Product Service.
Add the following files:
- model > product.py
- controller > product.py
Modify the main.py files, and add the following lines.
Let’s check the APIs.
Perfect. Our Product APIs working well.
Create the Auth Middleware.
We have created the login mechanism and the product APIs. Now we need to create the auth middleware to prevent unauthorized users from accessing our APIs.
To create the middleware, we gonna use the Python decorator feature.
Create auth files.
- middleware > auth.py
Modify the following files.
- controller > role_permission.py
- controller > token.py
- model > role_permission.py
Modify the main.py files and put the auth middleware.
- main.py
Let’s check the Postman.
Try to access the APIs without the token.
That is the kind of result you will receive. To send the token, we need to put the token inside the HTTP header as Bearer Token.
Bearer Token is a token that is commonly used to access protected resources.
Now try to access the APIs with tokens.
Work perfectly.
An important thing to remember when we validate the JWT is that we must verify the token signature.
Take a look at the following code from the token controller.
def _decode_token(self, token: str, secret_key):
payload = jwt.decode(
token,
secret_key,
algorithms=["HS256"],
verify=True
)
return payload
The verify = true means that the JWT decoder will make sure that the signature is matched with the token header and payload.
If the payload has changed, the signature will no longer be valid and it will raise InvalidToken errors.
Implement Token Revocation.
One of the major issues in JWT is that we can't invalidate the token, so it will remain alive until the expiration is over. So when the user logout from our app, the token is still usable.
As a solution, we need to blacklist the token until it is expired.
Every time we retrieve a token, we need to check whether the token is blacklisted or not.
The blacklisted token we store in the database must be cleared periodically, so we need to create an automatic task.
We gonna use the Python scheduler to create a cron job to automatically remove blacklisted tokens that are expired.
Let’s create 2 API endpoints in the main.py files.
Modify the following files:
- main.py
Add the following codes.
- controller > token.py
Add the following codes.
Add new files in the model folder.
- model > revoked_token.py
Modify the auth middleware.
- middleware > auth.py
Let’s do the logout actions.
Now use the token to access get product APIs.
It should return an error message: ‘Revoked token’.
The revoked token is stored in the database as a blacklisted token.
Now let's create a cron job to automatically remove the token when it expired.
Create Cron Job to Clear Blacklisted Tokens Periodically.
Open the cron folder and create the following files.
- requirements.txt
- scheduler.py
Create a virtual environment and install the dependencies.
python -m venv env
.\env\Scripts\activate
pip install -r requirements.txt
Set up the scheduler environment. Run the following commands.
SET CRON_USER=cron
SET CRON_PASSWORD=1234567
Run the scheduler.
python scheduler.py
Our token still has a longer lifetime, let's modify the expiration time so the scheduler could catch the tokens.
Update the blacklisted token expiration to the past so it will automatically be expired. Run the following queries.
UPDATE public.revoked_token
SET expired_at='2023-05-17 23:33:02+07'::timestamp;
select the data and make sure the expiration is changed.
Now just wait until the scheduler removes the expired tokens. It will take 5 minutes since we set the scheduler to run every 5 minutes.
It will show a print statement like this.
Now check the database.
The expired token is gone.
Perfect.
Implement Refresh Token.
Every time the token expired, the user is no longer able to access our application, they will be forced to log out from the system.
It’s not a good practice for user experience. The session should remain exists as long as the user opens the application.
That's why we should implement the refresh token. When the access token is going to expire or expired, use the refresh token to renew the tokens.
Refresh token and access token has a different lifetimes. In this tutorial, the access token will expire after 1 hour, and the refresh token will expire after 3 hours. As long as the refresh token is alive, the user could renew the tokens.
We have created the refresh token before, but not implement the refresh mechanism yet.
Let’s create the API to refresh the tokens.
Modify main.py. Add the following codes.
- main.py
Modify the controller also.
- controller > token.py
When the token is refreshed, the old one must be revoked.
Let’s check the APIs.
Work perfectly.
That's how we secure our back-end service using JWT.
We have created our restful APIs. To take full advantage of the JWT tokens, we need to know how to handle them on the front-end side.
Let's create the client apps to consume our APIs.
Creating UI using NextJs.
To create a NextJS application, run the following command.
npx create-next-app ui
Follow the instruction and wait until the project is created.
This is what the project looks like:
Modify the project structure.
Add the following folders.
- src > components
- src > services
- src > utils
Remove the following files and folders.
- src > pages > _document.js
- src > pages > api > hello.js
- src > pages > api
- src > styles > Home.module.css
Modify the following files.
- jsconfig.json
- next.config.js
- src > pages > app.js
- src > pages > index.js
Remove all the CSS inside src > styles > global.css.
Now your project should look like this one.
To execute the program, run the following command.
npm run dev
Try to access the apps from the browser.
Creating The Login Features.
This is how our login mechanism works.
We gonna hit the login APIs, if the login status is successful, it will return the access and refresh tokens.
Store the token inside cookies. Set the expiration times based on token expiration, so when the token is expired, the cookie will disappear.
Decode the token to get the user roles.
Use user roles to hit get permissions APIs.
Store the permission inside local storage so we could use it later.
Then we could redirect the user to the main page.
Let's create the scripts.
First, we need to create the pages for our application.
Add the following files inside the src > pages folders.
- login > index.js
- product > index.js
- index.js
Add the CSS files.
- styles > Login.module.css
Modify the CSS on global.css
- styles > global.css
Now, create the service to call the APIs.
Create the following files.
- services > auth.js
To work with other parts of our application such likes cookies and API calls, we need to create our custom library/modules.
Create the following files inside the utils folders.
- utils > api.js
This library will manage our basic API calls.
- utils > cookie.js
This library simplifies our cookie management such likes add a new cookie, getting and removing.
- utils > auth.js
This library simplifies our tasks such likes storing tokens/credentials, storing permissions, and getting the access token. All things about auth management will be handed here.
- utils > constant.js
It stores our app configuration.
- utils > lib.js
It stores our common helper functions.
Now it's completed. This is what the project looks like now.
It’s time to run the program. But before that, we need to install crypto-js, it used to generate hash for our unique cookie names.
npm install crypto-js
Then run the program.
*Make sure that your backend service is alive.
Try to access the login page, it will show you the page like this:
Put in your username and password. This is what I use.
username: admin
password: 1234567
If the login succeeds, it redirects you to the product page.
Perfect
Now it's time to create the product page.
Create Product Features.
First, create the pages for product features. Create the following files
- product > new.js
- product > [id].js
Modify the product index page.
- product > index.js
Add the CSS files.
- styles > Product.module.css
Create services to handle product API calls.
- services > product.js
Take a look at the result.
This is what the product home page looks like.
Try to add some data.
Work perfectly.
You could also try to delete and update the data.
Create The Auth Guard.
Our simple application is ready for now. But we haven't implemented the application access by the user permissions. Everyone could open the apps. We do not even check whether the user is login or not.
Let’s create a component to wrap our pages, if the user has no permission to the page, show the forbidden access page.
Create the following components.
- components > middleware > AuthGuard.js
- components > middleware > LoginGuard.js
- components > error > forbidden.js
Modify utils > auth.js and add the following functions.
- utils > auth.js
Modify the page and implement AuthGuard.
- pages > login > index.js
Modify and add the following lines.
- pages > product > index.js
Modify and add the following lines.
- pages > product > [id].js
Modify and add the following lines.
- pages > product > new.js
Modify and add the following lines.
Let’s take a look at the result.
Try to log in with the limited-access user.
I will use this credential.
username: user
password: 1234567
But before we log in to the app, we need to the open developer tools and clear the cookies and local storage data.
This is because we haven't created the log-out mechanism, this is the only way to could log out without waiting for the token to expire.
Open the developer tools.
Clear the Local Storage and Cookies.
Refresh the page and you'll automatically be redirected to the login page.
Try to log in.
Now you will see the following output.
As you can see, the action button is disabled because the user doesn't have the required permission.
And if we try to access the detail page or the new product page, it will return forbidden error pages.
And also if we go back to the login page, it will automatically redirect us to the home page because we already have a session.
Perfect.
Create Logout Features.
Create a new page for logout. Create a folder named log out inside the pages folders. Add index.js files inside it.
- logout > index.js
Modify the services > auth.js.
- services > auth.js
Since we have the logout endpoint, let’s modify the Auth Guard.
If the token is already expired, redirect the user to the log-out endpoint.
Perfect, the logout endpoint is ready.
Try to access the logout endpoint.
It will remove the permissions in local storage and clear your cookies.
Implement Automatic Token Renewal.
Our application has been working perfectly. After the user login, they will have a session for 1 hour.
After 1 hour, the access token will expire and the system will automatically logout.
At first sight, it doesn't seem to be a big deal. Most of the user leaves the website after a few minutes.
But what if the website is used on a daily basis, and the user takes a long time to work with it?
It will be annoying to re-login to the apps each hour. This is not a good user experience and soon the user will complain about your application.
We could handle this problem by implementing token renewal.
Create a background service on our front-end side to monitor the access token expiration.
If the token expiration will run out, send a request to get new tokens from backend services.
Now, it’s time to write the codes.
Modify the following files.
- utils > constant.js
- utils > auth.js
- services > auth.js
- components > middleware > AuthGuard.js
To check whether the background service is working or not, open the developer tools and modify the token expiration.
Speed up the expiration, you can reduce 30 to 40 minutes from the expiration times.
Actual expiration
After the modification.
Go to the network tab and watch the refresh token request. Soon you will see that the Front End side automatically sends the refresh token request.
Then, the expiration times will be updated.
Perfect.
Well done.
We have completed our JSON Web Tokens implementation on both Back End and the Front End side.
Thank you for reading my articles.
Leave a clap if you like it.
If you find that it is hard to follow the instructions or there is something stuck in your mind, don't hesitate to ask in the comment sections.
All the codes we used are available on my GitHub repositories.
Hope it is useful🙏.