Have you tried to integrate JWT authentication into your Node.js application, but never found the right solution? Then you have come to the right place. In this post, we are going to walk you through the finer details of JWT authentication in Node.js using the npm package, jsonwebtoken.
If you are still not sure about what exactly JWT is and how it works, you can follow our previous post, before continuing with the implementation. As we discussed in the previous post (A Brief Introduction to Securing Applications With JWT ), we will be following the best practices of JWT authentication in this implementation. In case you want to refresh your memory on JWTs first, let’s go through which best practices we are going to be following in this tutorial.
Send the JWT token in a cookie instead of the HTTP header
Set a short expiration time for the token
Use refresh tokens to re-issue access tokens that expire in a short time
There are two considerations I want to highlight before getting into the details:
Writing your own implementation for authentication is not always the best solution. There are several third parties offerings that can handle all of it for you in a very secure manner.
The code introduced in this tutorial is an implementation for a monolith application. If you want to use this code for a microservice you will have to use a combination of public/private keys to sign and verify the token.
Now that we have got the targets set, let’s start the implementation.
Initial preparations…
If you haven’t already, set up the Node environment for this project. Then, install the following packages that we will be using for this tutorial.
Express: The Node.js framework we will be using
Cookie-Parser: Since we will be sending the JWT token in a cookie, use this package to parse the cookie sent along with requests
Body-Parser: This package to parses the body of incoming requests to extract POST parameters
Dotenv: This package loads environment variables from .env file to the application environment
Json-Web-Token: This is the package that helps us with the JWT implementation
You can install these packages using the following command.
npm install express cookie-parser body-parser dotenv json-web-token --save
Now we’ll set up the backend of the application in the main file of the project, app.js.
require('dotenv').config()
const express = require('express')
const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser')
const app = express()
const {login, refresh} = require('./authentication') app.use(bodyParser.json())
app.use(cookieParser())
app.post('/login', login)
app.post('/refrsh', refresh)
In the next steps, we will implement the functions inside controller.js, which we have used in the above code. We use the login function to handle post requests sent to the /login route and log in users. We use refresh function to handle post requests sent to the /refresh route and issue new access tokens using the refresh token.
Set up environment variables
Before implementing the logic to log a user in, we need to set up the environment variables that are needed to configure JWTs. Create a .env file and add these two variables that we will be using inside the application.
ACCESS_TOKEN_SECRET=swsh23hjddnns ACCESS_TOKEN_LIFE=120 REFRESH_TOKEN_SECRET=dhw782wujnd99ahmmakhanjkajikhiwn2n REFRESH_TOKEN_LIFE=86400
You can add any string as the secret. It’s recommended to use a longer secret with random characters as a security measure. The expiration time of an access token we create will be 120 seconds. We also set the secret to signing the refresh token and its expiration time. Note how the refresh token has a longer lifetime compared to the access token.
Handle user login and JWT token creation
Now we can get to the step of implementing the login function we imported to the app.js file to handle the /login route.
We are going to store an array of user objects in the application for the purpose of this implementation. In a real-world scenario, you will be retrieving this user information from a database or any other location. Also, this is for demo purposes only, NEVER EVER store the actual passwords.
Don’t save passwords in plain text
let users = {
john: {password: "passwordjohn"},
mary: {password:"passwordmary"}
}
When implementing the login function, first we need to retrieve the username and password sent with the login POST request.
const jwt = require('json-web-token')
// Never do this! let users = {
john: {password: "passwordjohn"},
mary: {password:"passwordmary"} }
exports.login = function(req, res){
let username = req.body.username let password = req.body.password
// Neither do this!
if (!username || !password || users[username] !== password){ return res.status(401).send()
}
}
If the request doesn’t contain either the username or the password, the server responds with a 401 unauthorized status. The same response applies if the password sent with the request does not match the password stored in the database for that particular username.
If the client has sent correct credentials to the server, then we proceed to log in the user to the system by issuing new JWT tokens. In this case, the new logging-in user receives two tokens: access token and refresh token. The access token is then sent along with the response inside a cookie back to the client. The refresh token is stored in the database for issuing access tokens in the future. In our case, we will store the refresh token in the user array we previously created.
const jwt = require('json-web-token')
// Never do this! let users = {
john: {password: "passwordjohn"},
mary: {password:"passwordmary"} }
exports.login = function(req, res){
let username = req.body.username let password = req.body.password
// Neither do this!
if (!username || !password || users[username].password !== password){ return res.status(401).send()
}
//use the payload to store information about the user such as username, user role, etc. let payload = {username: username}
//create the access token with the shorter lifespan
let accessToken = jwt.sign(payload, process.env.ACCESS_TOKEN_SECRET,
{
algorithm: "HS256",
expiresIn: process.env.ACCESS_TOKEN_LIFE
})
//create the refresh token with the longer lifespan
let refreshToken = jwt.sign(payload, process.env.REFRESH_TOKEN_SECRET,
{
algorithm: "HS256",
expiresIn: process.env.REFRESH_TOKEN_LIFE
})
//store the refresh token in the user array users[username].refreshToken = refreshToken
//send the access token to the client inside a cookie res.cookie("jwt", accessToken, {secure: true, httpOnly: true})
res.send() }
When sending the access token inside a cookie, remember to set the httpOnly flag to prevent attackers from accessing the cookie from the client-side. We have also set the secure flag in the above example. However, if you are only trying out this code over an HTTP connection, not an HTTPS connection, remove the secure flag to send it with the response.
Add a middleware to authenticate user requests
The server needs to check whether a user has logged in before giving access to certain routes. We can use the access token sent in a cookie with every request to verify that the user is, in fact, authenticated. This process is carried out in a middleware.
Let’s create a new file named middleware.js and implement the verify method to check whether the user is authenticated.
First, we should retrieve the access token from the cookie sent with the request. If the request does not contain the access token, it will not proceed to the intended route and instead, return a 403 forbidden error.
const jwt = require('json-web-token')
exports.verify = function(req, res, next){
let accessToken = req.cookies.jwt
//if there is no token stored in cookies, the request is unauthorized if (!accessToken){
return res.status(403).send()
}
}
If the request contains the access token, then the server will verify whether it was issued by the server itself using the stored secret. In case the token is expired or recognized as a one not signed by the server, the jsonwebtoken’s verify method will throw an error. We can handle the error to return a 401 error back to the client.
const jwt = require('json-web-token')
exports.verify = function(req, res, next){
let accessToken = req.cookies.jwt
//if there is no token stored in cookies, the request is unauthorized if (!accessToken){
return res.status(403).send()
}
let payload try{
//use the jwt.verify method to verify the access token
//throws an error if the token has expired or has a invalid signature payload = jwt.verify(accessToken, process.env.ACCESS_TOKEN_SECRET) next()
}
catch(e){
//if an error occurred return request unauthorized error return res.status(401).send()
}
}
Now we can use this middleware to protect any route that requires the users to be logged in before accessing. Import the middleware to the place you are handling routes, which, in our case, is app.js. If we are trying to protect a route named /comments, it can be easily achieved by adding the middleware before the route handler.
const {verify} = require('./middleware') app.get('/comments', verify, routeHandler)
The request will be passed on to the route handler only if the user is authenticated.
Issuing new access token using the refresh token
Remember the /refresh route and refresh function we had used in the initial code in the app.js file? Now we can implement this refresh function to issue new access tokens using the stored refresh token.
The function used here, refresh, is also inside the controller.js file we used before for the login function implementation.
The first part of this function is much like what we did to verify an access token: Check whether the access token was sent and verify the token.
exports.refresh = function (req, res){
let accessToken = req.cookies.jwt if (!accessToken){
return res.status(403).send()
}
let payload try{
payload = jwt.verify(accessToken, process.env.ACCESS_TOKEN_SECRET) }
catch(e){
return res.status(401).send()
}
}
Then, we are going to retrieve the refresh token of this particular user using the username stored in access token’s payload. Once the refresh token is verified, the server will issue a new access token as implemented in the login function.
If the refresh token is expired or fails to verify, the server will return an unauthorized error. Otherwise, the new access token will be sent in the cookie.
exports.refresh = function (req, res)
{
let accessToken = req.cookies.jwt if (!accessToken){
return res.status(403).send()
}
let payload try{
payload = jwt.verify(accessToken, process.env.ACCESS_TOKEN_SECRET) }
catch(e){
return res.status(401).send()
}
//retrieve the refresh token from the users array
let refreshToken = users[payload.username].refreshToken
//verify the refresh token try{
jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET)
}
catch(e){
return res.status(401).send()
}
let newToken = jwt.sign(payload, process.env.ACCESS_TOKEN_SECRET, {
algorithm: "HS256",
expiresIn: process.env.ACCESS_TOKEN_LIFE
})
res.cookie("jwt", newToken, {secure: true, httpOnly: true}) res.send()
}
This step completes our implementation of JWT authentication with Node.js
Summary
In this tutorial, we went through the steps of implementing authentication with JWT in Node.js. As a continuation of our previous post, where we discussed the theories behind JWT authentication, our implementation was focused on adhering to the best practices we discussed before. As a result, our JWT implementation made use of cookies to send JWTs and refresh tokens to generate new access tokens. If you are willing to take a step ahead of this implementation, you can come up with a solution to re-issue refresh tokens within a short timespan to avoid security breaches.
Source: Livecodestream
Commentaires