Authentication with JWT in Node.js
Session-based and token-based authentications are two ways of making sure that a user is who they say they are.
Sessions can be stored in memory or can be persisted in a database.
Token based authentication methods have a piece of string called token (can be encrypted or unencrypted) in the request header which lets the server know of the user or, at least, it ensures that the client making the request is legit.
1. JWT
One frequently used way of token-based authentication methods involves the use of JWTs.
JWT stands for JSON Web Token and it’s an open standard for securely transmitting information between client and server. JWTs are signed with a secret or a public/private key pair, and they ensure the integrity of the request.
JWTs consist of a header (before the first dot), a payload (between the two dots) and a signature part (after the second dot), and they look like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
The header contains the algorithm and the token type. They payload can carry information - among others - about the client, the expiration and the time of the token was issued, while the signature confirms that the token is genuine. But, anything can be put in the payload as long as the information is not sensitive.
JWTs can be viewed in the jwt.io website, where payloads can be decoded and signatures can be verified.
JWTs make it possible for the authentication process to involve no sessions or databases, hence the server is stateless (it doesn’t know about session information).
2. Applying JWTs
I’ll rely on the Node.js and Express.js server I wrote about in the abovementioned posts, and I won’t repeat each step here, only those ones that are different from the original. The full code can be found at the bottom of the page.
2.1. Install jsonwebtoken
I’ll use the jsonwebtoken
library to generate, sign and verify JWTs, so the library needs to be installed to the project folder:
npm install jsonwebtoken
Let’s require
it at the top of server.js
:
// ...
const jwt = require('jwt');
// ...
We’ll also need a secret which the tokens will be signed with. Ideally, they are stored in a secure place like AWS Parameter Store, and fetched from there.
In this case, the secret will be a secret string:
const secret = `secret-string` // stored in a secure place and fetched from there;
2.2. authorize middleware
A piece of middleware function can be created to verify the token and can be added to endpoints that need to be protected and only used by authenticated users.
The authorize
middleware can look like:
const authorize = (req, res, next) => {
const authHeader = req.headers['authorization'];
if (!authHeader) {
return next(new CustomError('Unauthorized', 401));
}
const bearer = authHeader.split(' ');
const token = bearer[1];
try {
const decoded = jwt.verify(token, secret);
console.log('user authenticating:', decoded);
req.user = decoded;
return next();
} catch (e) {
return next(new CustomError('Unauthorized', 401));
}
};
The token can be transmitted in the Authorization
header. If the header is not present, the server will return a 401
error.
If it’s there, the header can look like this:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
The value of the header needs to be split at the whitespace between the word Bearer
and the token itself, and then the second element of the resulting array (which is the token itself) will be verified.
The verify
method will throw an error if the verification is unsuccessful, and we can send the error to the error handler function.
If the token can be decoded, we can save the decoded value to the user
property of the request. The token will contain the username
and it will be added in the /login
handler.
2.3. Signing the token
The JWT will be signed in the /login
route handler after the user authentication (correct username and password) is successful:
const signed = jwt.sign({
username: user,
exp: Math.floor(Date.now() / 1000) + 3600,
}, secret);
console.log('Authentication successful');
res.status(200).json({ token: signed });
The sign
method accepts the secret and the payload for the token.
The payload will be an object, which has the username
property and the exp
expiration time in seconds (it’s one hour in this case). After one hour the token will become invalid, and the verify
method in the authorize
middleware will throw an error, which makes it impossible for the user to log in.
2.4. The code
The full code can look like this:
const express = require('express');
const { MongoClient } = require('mongodb');
const bodyParser = require('body-parser');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const secret = 'secret-string'; // to be stored in a secret place
const app = express();
const PORT = 3000;
const MONGO_URL = 'mongodb://localhost:27017';
app.use(bodyParser.json());
class CustomError extends Error {
constructor(message, status) {
super(message);
this.status = status;
this.message = message;
}
}
const authorize = (req, res, next) => {
const authHeader = req.headers['authorization'];
if (!authHeader) {
return next(new CustomError('Unauthorized', 401));
}
const bearer = authHeader.split(' ');
const token = bearer[1];
try {
const decoded = jwt.verify(token, secret);
console.log('user authenticating:', decoded);
req.user = decoded;
return next();
} catch (e) {
return next(new CustomError('Unauthorized', 401));
}
};
async function main() {
let client
try {
client = await MongoClient.connect(MONGO_URL)
console.log('Connected to database')
} catch (e) {
throw new CustomError('Error while connecting to database')
}
const db = client.db('authentication-with-node')
// register endpoint
app.post('/register', async (req, res, next) => {
const username = req.body.username
const password = req.body.password
if (!username || !password) {
return next(new CustomError('No username or password', 404))
}
if (password.length < 10) {
return next(new CustomError('Password must consist of at least 10 characters'))
}
const isExistingUser = await db.collection('users').find({ username })
if (isExistingUser) {
return next(new CustomError('User already registered'))
}
try {
const hashedPassword = await bcrypt.hash(password, 10)
await db.collection('users').insertOne({
username,
password: hashedPassword,
})
console.log('User data have been saved')
res.redirect('/login')
} catch (e) {
return next(new CustomError('Unknown error'))
}
})
// public endpoint
app.get('/', (req, res) => {
res.send('Public endpoint')
})
// login endpoint
app.post('/login', async (req, res, next) => {
const { username, password } = req.body;
if (!(username && password)) {
return next(new CustomError('Missing username or password'));
}
try {
const { username: user, password: hashedPassword } = await db.collection('users')
.findOne({ username });
if (username !== user) {
return next(new CustomError('Incorrect username/password'));
}
const isMatchingPassword = await bcrypt.compare(password, hashedPassword);
if (!isMatchingPassword) {
return next(new CustomError("Incorrect username/password"));
}
const signed = jwt.sign({
username: user,
exp: Math.floor(Date.now() / 1000) + 3600,
}, secret);
console.log('Authentication successful');
res.status(200).json({ token: signed });
} catch (e) {
console.log(e)
return next(new CustomError('Error while logging in'));
}
});
// private endpoint
app.get('/private', authorize, (req, res, next) => {
// middleware handles errors
res.send('Private endpoint hit');
});
app.get('/logout', authorize, (req, res, next) => {
console.log('Logging out');
res.redirect('/');
});
// error handling
app.use((err, req, res, next) => {
res.status(err.httpStatusCode || 400).json({
message: err.message,
status: err.status,
});
});
app.listen(PORT, () => {
console.log(`Server is listening on ${ PORT }`);
})
}
main().catch((e) => {
console.log(e);
});
2.5. Try it out
Let’s try if the token-based authentication works! (It should.)
First, start the MongoDB database:
docker-compose up -d
The database and server setup can be found in this post.
Now the server can be started:
node server.js
We can now log in James Bond:
`curl -X POST -H 'Content-Type: application/json' \\
-d '{"username": "james.bond", "password": "mysupersecretpassword"}' localhost:3000/login`
The route handler sends the token back:
res.status(200).json({ token: signed });
As such, the response should look like this:
{
"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImphbWVzLmJvbmQiLCJleHAiOjE1NzQ0NjA4ODgsImlhdCI6MTU3NDQ1NzI4OH0.mSfwEijOmDzOS5BUa_pr6ORIG6Lkr1YpgoqzsM2lYWw"
}
If this token is pasted in the token decoder, we’ll get the payload back:
{
"username": "james.bond",
"exp": 1574460888,
"iat": 1574457288
}
That’s almost magic.
What even more magic is that the /private
endpoint can be hit and the authorize
middleware will verify the token:
curl -H 'Content-Type: application/json' \\
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImphbWVzLmJvbmQiLCJleHAiOjE1NzQ0NjA4ODgsImlhdCI6MTU3NDQ1NzI4OH0.mSfwEijOmDzOS5BUa_pr6ORIG6Lkr1YpgoqzsM2lYWw' \\
localhost:3000/private
When we curl the endpoint, we’ll need to provide the token in the Authorization
header. In a real world application the token will be placed in the request header in the client side (e.g. a React or Angular).
Now let’s try to access the /private
endpoint without the token in the header. The authorize
middleware should prevent the user from accessing the private content:
curl -H 'Content-Type: application/json' localhost:3000/private
The response should be Unauthorized
with a 401
status code.
3. Summary
Authentication with JWT is a very popular way to making sure that the user is who they claim to be.
JWT stands for JSON Web Token and it’s an open standard. The jsonwebtoken
library is one that can perform the signature and the verification of the token. It’s a good idea to create middleware function to verify the token and attach the middleware to various endpoints that need to be protected.
Thanks for reading and see you next time.