Skip to content

JSON Web Tokens

Introduction

JSON Web Tokens (JWT) are used with authentication in web applications. JWT is used to send information that can be verified like a digital signature in server side application. JWT is used usually with HTTP Authorization headers with web request to server side. It is represented as a sequence of base64url encoded values that are separated by period characters.

For example Node.js application are stateless, so developers needs some technology to authenticate client application request to server side. Token based JWT authentication is stateless, so there is no need to store information in any session to the server side.

Process:

  • User will log to web application with username and password
  • Server will indentify the user and will return JWT token from the server side to the client application
  • Client application stores the JWT client to the application state or for example to the local storage
  • Client application will send requests to the server side with the JWT token
  • Server side application will validate the JWT token and will respond to the client side application

Look process diagram from Authentication material.

This is how nowadays applications will use JWT token to check authentication and request made by the client side application. Now the same token can be used with other domains where the user is logged in at the first place. This way we can scale up our application.

JWTs consist

This is one example how a JSON Web Token looks like (new lines inserted at dots to improve readability). It is three Base64-URL strings separated by dots that can be easily passed in HTML and HTTP environments.

1
2
3
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

This might look like garbage, but it is actually a compact presentation of a series of claims, paired with a signature to verify authencity.

JWTs consist of three parts separated by dots.

header

The header typically consists of two parts: the type of the token, which is JWT, and the signing algorithm being used, such as HMAC SHA256 or RSA.

1
2
3
4
{
  "alg": "HS256",
  "typ": "JWT"
}

payload

The second part of the token is the payload, which contains the claims. Claims are statements about an entity (typically, the user) and additional data. There are three types of claims: registered, public, and private claims.

1
2
3
4
5
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

Signature

To create the signature part you have to take the encoded header, the encoded payload, a secret, the algorithm specified in the header, and sign that.

1
2
3
4
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

How do JSON Web Tokens work?

In authentication, when the user successfully logs in using their credentials, a JSON Web Token will be returned. Since tokens are credentials, great care must be taken to prevent security issues. In general, you should not keep tokens longer than required. You can store it for example React application state. So it will be available when user is using an application.

Look process diagram from Authentication material.

Whenever the user wants to access a protected route or resource, the user agent should send the JWT, typically in the Authorization header using the Bearer schema.

1
Authorization: Bearer <token>

The server's protected routes will check for a valid JWT in the Authorization header, and if it's present, the user will be allowed to access protected resources. If the JWT contains the necessary data, the need to query the database for certain operations may be reduced, though this may not always be the case.

Example project

Let's create a small demo app to test JWT with Node.js application.

Create a new project and install packages

First create a new Node.js project. Install a few packages

We will use a following packages:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
mkdir jwtdmeo
cd jwtdemo
npm init -y
npm i express
npm i jsonwebtoken
npm i mongoose
npm i dotenv
npm i bcrypt
npm i mongoose-unique-validator
npm i --save-dev nodemon

Environment variables and server secrets

Create .env file to store server side secrets. You will need to generate a server side secret string to use JWT. Basicly you can use any string, but you can use Node to generate this string to you. Creating a random string for the server secret can be done in the Node.js REPL with the crypto module.

Launch a Node without any JavaScript file:

1
2
3
4
5
6
> node 
Welcome to Node.js v18.12.1.
Type ".help" for more information.
> require('crypto').randomBytes(64).toString('hex')
'fad0968850a2fa26a0adbb13f6ebcc67eb7140faaf6b801eeae2044bf2dec2689cff275cd1e7fae4b296396f23269363527d06650eb1bbfcb4c2f9d1075bf358'
> 

Add generated secret string and .env file (and other needed variables):

.evn
1
2
3
PORT=3000
MONGODB_URI='YOUR_MONGODB_CONNECTION_STRING_HERE'
ACCESS_TOKEN_SECRET='fad0968850a2fa26a0adbb13f6ebcc67eb7140faaf6b801eeae2044bf2dec2689cff275cd1e7fae4b296396f23269363527d06650eb1bbfcb4c2f9d1075bf358'

Create a User Model

Create a User Model to store User username and passwordHash. Use Mongoose Unique Validator to validate unique user username. User will have passwordHash, which will be created with bcrypt.

When User is asked as a JSON, transform is used to remove passwordHash from the returning JSON data / document. SO, password hash string is not visible at any UI or can't be get by the caller client. Read more about Transform.

models/user.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const mongoose = require('mongoose');
const uniqueValidator = require('mongoose-unique-validator');
const Schema = mongoose.Schema;

const UserSchema = new Schema({
  username: {
    type: String,
    required: true,
    unique: true,
  },
  passwordHash: String
});

UserSchema.set('toJSON', {
  transform: (document, returnedObject) => {
    delete returnedObject.passwordHash
  }
})

UserSchema.plugin(uniqueValidator)

const UserModel = mongoose.model('User', UserSchema, "JWTUsers");
module.exports = UserModel;

Create a connection to MongoDB

Create index.js file to your project, use User Model and make a connection to MongoDB.

index.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
require('dotenv').config();
const jwt = require("jsonwebtoken");
const mongoose = require('mongoose');
const express = require('express'); 
const app = express();
app.use(express.json());
const port = process.env.PORT;

// Parse requests of content-type - application/json
app.use(express.json());

// Include UserModel to communicate with MongoDB
const User = require("./models/user");

// Connect your app to MongoDB and get connection, fix deprecation warnings
mongoose.set('strictQuery', false) 
mongoose.connect(process.env.MONGODB_URI, 
  { 
    useNewUrlParser: true, 
    useUnifiedTopology: true
  }
);
const db = mongoose.connection
db.on('error', console.error.bind(console, 'MongoDB connection error:'));
db.once('open', () => console.log('Database successfully connected!'));

// listen port
app.listen(port, () => {
  console.log(`Example JWT app listening on port ${process.env.PORT}`);
})

Register/Add a new user

Create a new users.js file to your controllers folder. Create a new endpoint for POST method, which will be used to add a new user to MongoDB. Use bcrypt to hash user password and create a new user. Save user to MongoDB.

controllers/users.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const bcrypt = require('bcrypt');
const usersRouter = require('express').Router();
const User = require('../models/user');

usersRouter.post('/', async (req, res, next) => {
  const { username, password } = req.body;
  const saltRounds = 10;
  const passwordHash = await bcrypt.hash(password, saltRounds);

  const user = new User({
    username: username,
    passwordHash: passwordHash
  });

  try {
    const savedUser = await user.save()
    res.json(savedUser);
  } catch(error) {
    next(error);
  }

})

module.exports = usersRouter;

Use users router in your index.js.

index.js
1
2
3
// use users route
const usersRouter = require('./controllers/users');
app.use('/api/users', usersRouter);

Use Postman to register a new user. Note that returned user JSON doesn't have passwordHash field in JSON. Click image to see it bigger!

!Image 01

Look your MongoDB and find your stored user. Note that password is hashed.

!Image 02

Possible errors with a saving will be now send to middleware. Create a new middlewares folder and a new errorHandler.js file to detect errors with mongoose-unique-validator.

middlewares/errorHandler.js
1
2
3
4
5
6
7
8
const errorHandler = (err, req, res, next) => {
  if (err.name === 'ValidationError') {
    return res.status(400).json({ error: err.message });
  }
  next(err);
}

module.exports = errorHandler;

Remember use created errorHandler.js middleware in your index.js.

index.js
1
2
3
// use error handler middleware
const errorHandler = require('./middlewares/errorHandler');
app.use(errorHandler);

Try to add another user with a same username and check how mongoose-unique-validator will detect attempt to create a new user with same username.

!Image 03

Create a Login route and get JWT token

Create a new login route controller controllers/login.js to your Node.js application. First user will be found from MongoDB with findOne function.

First user will be found from the MongoDB and then password correction will be checked with bcrypt.compare function. Unauthorized statuscode 400 will be returned if users's password is not correct one. A new JWT token will be created, if username and password are correct with jwt.sign() function. Token, username and id will be send back to the client.

controllers/login.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const loginRouter = require('express').Router();
const User = require('../models/user');

loginRouter.post('/', async (req, res) => {
  const { username, password } = req.body;

  const user = await User.findOne({ username: username });
  if (user) {
    const validPassword = await bcrypt.compare(password, user.passwordHash);
    if (validPassword) {
      const payload = {
        "username": user.username,
        "id": user._id
      }
      const token = jwt.sign(payload, process.env.ACCESS_TOKEN_SECRET);
      res.status(200).send({ token, username: user.username, id: user._id});
    } else {
      res.status(400).json({ error: "Username or password is invalid!" });
    }
  } else {
    res.status(401).json({ error: "User does not exist!" });
  }

})

module.exports = loginRouter

Use login router in your index.js.

index.js
1
2
3
// use login route
const loginRouter = require('./controllers/login');
app.use('/api/login', loginRouter);

Use Postman to send login request to server side. JWT token will be send to the client. Client should store this client somehow and it need to be used with other request to server side. Click image to see it bigger!

!Image 04

You should see a token in a Postman.

1
2
3
4
5
{
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlcHBvIiwiaWQiOiI2M2RhODNiZWRhZTMxOTkwNTIwYzAyZWMiLCJpYXQiOjE2NzUyNjU4NTJ9.lqTPeuH4uRC05d7WcsVRHKgIamvhl_AN4YdUVUTZmuA",
    "username": "teppo",
    "id": "63da83bedae31990520c02ec"
}

You can test your token with jwt.io to get a look at the payload included in the token. Just copy and paste your token there.

!Image 05

Request endpoint with JWT token

Create a new data route controller controllers/data.js to your Node.js application. This endpoint will be used to test request with JWT token. JWT Token will be get with Bearer authentication type. Token will be validated with jwt.verify() function.

controllers/data.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const jwt = require('jsonwebtoken');
const dataRouter = require('express').Router();

dataRouter.get('/', (req, res) => {
  // get authorization token
  const authorization = req.headers.authorization;
  if(!authorization || !authorization.startsWith('Bearer')) {
    res.status(401).json({ error: 'User token missing or invalid!' });
  } else {
    // get token
    const token = authorization.split(' ')[1];
    try {
      // verify token
      const payload = jwt.verify(token,process.env.ACCESS_TOKEN_SECRET);
      // send response back to client
      res.status(200).json({ msg:`GET request with ${payload.username} successfully received!`})  
    } catch (error) {
      res.status(401).json({ error: 'JWT token is unauthorized!' })
    }
  }
})

module.exports = dataRouter;

Use data router in your index.js.

index.js
1
2
3
// use data route
const dataRouter = require('./controllers/data');
app.use('/api/data', dataRouter);

Use Postman to send get request to server side with JWT token. Server should send data back if authorization is successfull.

!Image 06

Try to access /api/data endpoint also without a valid JWT token or without token.

!Image 07

!Image 08

Authorization middleware

Now authorization is done in one route and inside it's controller. How about if you need authorization in multiple routes? Do you just copy and paste authorization code to every route - hope not! So it would be a good idea to move authorization to middleware and use it before every route that needs a authorization.

Create a new auth.js to your middlewares folder.

middlewares/auth.js
1
2
3
4
5
6
7
8
const auth = async (req,res,next) => {
  // we are now only logging auth token
  console.log(req.headers.authorization);
  // invoke the next middleware function in the app
  next();
}

module.exports = auth;

And require your authorization middleware in your index.js before earlier made data route AND use it in your protected /api/data route before dataRouter.

index.js
1
2
3
4
5
6
// use auth middleware
const auth = require('./middlewares/auth');

// use data route AND auth 
const dataRouter = require('./controllers/data');
app.use('/api/data', auth, dataRouter);

Now everytime a client send request to /api/data route, request goes through the created auth middleware first. In the middleware we have used a next() function, so the control is passed on to the dataRouter controller.

Send a get request to /api/data route from Postman and look your terminal window. It should show a token sent from Postman. Of course user need to be logged in and use correct token.

!Image 09

Ok, now we know how auth middleware will be called before needed protected routes. So we need to check JWT token in our auth middleware. Modify your middlewares/auth.js to include authorization code from previously made data controller.

middlewares/auth.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const jwt = require('jsonwebtoken');

const auth = async (req,res,next) => {
  // get authorization token
  const authorization = req.headers.authorization;
  if (!authorization || !authorization.startsWith('Bearer')) {
    res.status(401).json({ error: 'User token missing or invalid!' });
  } else {
    // get token
    const token = authorization.split(' ')[1];
    try {
      // verity token
      const payload = jwt.verify(token,process.env.ACCESS_TOKEN_SECRET);
      // add data to request
      req.payload = payload;
      // go to next middleware
      next();
    } catch (error) {
      res.status(401).json({ error: 'JWT token is unauthorized!' })
    }
  }
}

module.exports = auth;

Modify your data.js controller to only send data back to client.

controllers/data.js
1
2
3
4
5
6
7
8
const dataRouter = require('express').Router();

dataRouter.get('/', (req, res) => {
  // send response back to client, get data from request
  res.status(200).json({ msg:`GET request with ${req.payload.username} successfully received!`})  
})

module.exports = dataRouter;

Final words

This was just a quick look to JWT. There are still much more to learn like handle token expiration time or how to renewing a token etc.. You can learn those consepts by yourself.

Read more

Goals of this topic

Understand

  • Authentication and authorization with JWT Tokens.