Skip to content
bobby_dreamer

JWT - Json Web Tokens usage in NodeJS

notes, nodejs, security3 min read

JWT are usually created during logins returned by the server on success and saved in clients in local storage or session or mostly in cookies. Its used when client tries to access protected route. JWT is not replacement for authentication, it just verifies that this is the client who logged in.

Properties/Features of JWT

  • Its a Base64 Encoded URL safe string.
  • It is Encoded and NOT Encrypted
    • Should not save sensitive information inside a JWT.
  • Typical JWT: xxxx.yyyy.zzzz (Header.Payload.Signature)
    • Header: Algorithm used (HMAC SHA256) and type of token (JWT).
    • Payload: Contains claims
    • Signature: Used to verify if the token wasn't changed along the way.

Token Flow

First, just know there are two types of tokens,

  1. Refresh tokens which expires in one year, it is used to regenerate new set of access and refresh tokens.
  2. Access tokens expires based on requirement 1hr to 24h usually. It is used to access protected routes.

In the below approach,

  1. Client tries to make a Login/Register request with username & password
  2. Server checks if it is valid.
    • If valid, it sends Access-Token & Refresh-Token
  3. For accessing protected route, client will his send his Access-Token as Authorization Header to server
  4. Server verifies whether the authorization token is valid.
    • If valid, server will respond saying its valid
    • Otherwise it will send "not valid" with 401 Forbidden.
  5. If client gets, Acccess-Token has expired from server, it will send request with refresh token in the request body.
  6. If the Refresh token is authorized, server will send back new Access-Token and new Refresh-Token.
    • If the Refresh-Token is invalid, we send back not valid and 403 Unauthorized state.

Packages beings used are

1npm install --save express morgan http-errors dotenv jsonwebtoken nodemon bcrypt

Programs

Below program is run on two scenarios

  1. Generate the initial Refresh token and Access token which are saved into .env file
  2. If and When, the Refresh token and Access token are compromised, run this program to regenerate tokens and update .env file
generate-token.js
1const crypto = require('crypto')
2
3const key1 = crypto.randomBytes(32).toString('hex')
4const key2 = crypto.randomBytes(32).toString('hex')
5console.table({ key1, key2 })
6
7## Output
8D:\BigData\14.Nodejs\16.JWT\helpers>node generate-token.js
9┌─────────┬────────────────────────────────────────────────────────────────────┐
10 (index)Values
11├─────────┼────────────────────────────────────────────────────────────────────┤
12│ key1 │ '8681e735b3030348abc773be0ff17e04659d6c731a5e55ea3c54207c527ca4d6'
13│ key2 │ '2b51670f780250f02419ede7eaf55232c167509842cb12d5493b57e9ae3d8c5f'
14└─────────┴────────────────────────────────────────────────────────────────────┘

Below are the contents of .env file

.env
1PORT=3000
2ACCESS_TOKEN_SECRET=8681e735b3030348abc773be0ff17e04659d6c731a5e55ea3c54207c527ca4d6
3REFRESH_TOKEN_SECRET=2b51670f780250f02419ede7eaf55232c167509842cb12d5493b57e9ae3d8c5f

All the codes are available in Github. In this sample application, user data is stored in a JSON file during registration and during login the same file is read. We will just discuss only the main part of the codes.

Below is the code used to generate access token during login/register. NodeJS code for access and refresh token are almost same just the expiresIn changes. In access its 15s here below quick testing and refresh can be set from range 7d(7 days) to 1y(1 year). It follows vercel/ms or zeit/ms convention.

Don't store any authorized/secret information in payload or options object as they are only encoded not encrypted.

helpers\jwt_helper.js
1...
2let signAccessToken = (userId) => {
3 return new Promise((resolve, reject) => {
4 const payload = {}
5 const secret = process.env.ACCESS_TOKEN_SECRET
6 // const secret = "Some Super Secret"
7 const options = {
8 expiresIn: '15s',
9 issuer: 'bobbydreamer.com',
10 audience: userId, /* who this token is intended for */
11 }
12 JWT.sign(payload, secret, options, (err, token) => {
13 if (err) {
14 console.log(err.message)
15 reject(createError.InternalServerError())
16 return
17 }
18 resolve(token)
19 })
20 })
21}
22...

In the below, i have sent post request to login route(left-bottom). On the right it has generated a acccess token. VSCode REST Test

You can copy the above access token from above enter it in site jwt.io to decode the string and get all the payload information. From https://jwt.io/

This access token can be stored in cookie/session/localStorage, it upto to the user. Its mostly stored in the cookie, so its sent to server on each request. So, one of the recommendation here is to keep the payload small as possible.


JWT Error handling

There are 3 types of error codes,

  • TokenExpiredError

    • Has only one message - 'jwt expired'
  • JsonWebTokenError

    • This has multiple messages and passing these messages out to client might open a security hole. So in the code, we are just mentioning it as 'Unauthorized'
      • 'jwt malformed'
      • 'jwt signature is required'
      • 'invalid signature'
      • 'jwt audience invalid. expected: [OPTIONS AUDIENCE]'
      • 'jwt issuer invalid. expected: [OPTIONS ISSUER]'
      • 'jwt id invalid. expected: [OPTIONS JWT ID]'
      • 'jwt subject invalid. expected: [OPTIONS SUBJECT]'
  • NotBeforeError

    • Has only one message - 'jwt not active'
helpers\jwt_helper.js
1...
2let verifyAccessToken = (req, res, next) => {
3 if (!req.headers['authorization']) return next(createError.Unauthorized())
4
5 const authHeader = req.headers['authorization']
6 const bearerToken = authHeader.split(' ')
7 const token = bearerToken[1]
8
9 JWT.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, payload) => {
10 if (err) {
11 const message =
12 err.name === 'JsonWebTokenError' ? 'Unauthorized' : err.message
13 return next(createError.Unauthorized(message))
14 }
15 req.payload = payload
16 next()
17 })
18}
19...

In the below test code, the refresh and access tokens are just retured back to the client

routes\Auth.Route.js
1router.post('/login',async(req, res, next) =>{
2 try{
3 const {email, password} = req.body;
4 if(!email || !password) throw createError.BadRequest()
5
6 let data = await readFile();
7 // console.log(data);
8 //Just read the length of data in file to see if its empty or not
9 if(Object.keys(data['content']).length > 0)
10 data = JSON.parse(data['content']);
11 else throw createError(400, "Please register");
12
13 console.log(`All Data=${JSON.stringify(data)}`);
14
15 let temp = await validateUserPassword(data, email, password);
16 // console.log(temp);
17 if(temp=="USER NOT FOUND"){
18 throw createError.NotFound("Please register");
19 }else if(temp=="NOT MATCHED"){
20 // console.log(temp);
21 throw createError.Unauthorized("username/password doesn't match");
22 // res.status(400).send("Password doesn't match");
23 }else if(temp=="MATCHED"){
24 // console.log(temp);
25 const accessToken = await signAccessToken(email)
26 const refreshToken = await signRefreshToken(email)
27
28 // res.status(200).send("A Successfully logged in");
29 res.status(200).send({ accessToken, refreshToken })
30 }
31 }catch(error){
32 console.log(`login-Catch : ${error}`);
33 next(error);
34 }
35});

Below is the register code, it is similar to above login route but there few differences like

  1. In the login route, users.json file is just read, here it is read and does few things extra
  2. Encrypting password using bcrypt. bcrypt is another library which is used here, it has nothing to do with JWT. Just a little learning. Encryption is one-way meaning, you can only encrypt and while login you compare password string with hashing password string stored in file/server.
routes\Auth.Route.js
1router.post('/register',async(req, res, next) =>{
2 // console.log(req.body);
3 // res.send('Register route');
4 try{
5 const {email, password} = req.body;
6 if(!email || !password) throw createError.BadRequest()
7
8 let data = await readFile();
9 // console.log(data);
10 //Just read the length of data in file to see if its empty or not
11 if(Object.keys(data['content']).length > 0)
12 data = JSON.parse(data['content']);
13 else data = data['content'];
14
15 console.log(`Data=${JSON.stringify(data)}`);
16
17 let temp = await findUser(data, email);
18 // let temp = await findData(data, email, password);
19 // console.log(temp);
20 if(temp=="FOUND"){
21 throw createError.Conflict(`${email} is already been registered`)
22 }
23
24 //Below is the "NOT FOUND" logic
25 // data[email] = password;
26
27 //Encrypting Password
28 const salt = await bcrypt.genSalt(10);
29 const hashedPassword = await bcrypt.hash(password, salt);
30 data[email] = hashedPassword;
31
32 data = JSON.stringify(data)
33 console.log(`Updated data=${data}`);
34 temp = await writeFile(data);
35 if(temp) console.log('Created user '+email);
36
37 const accessToken = await signAccessToken(email);
38 const refreshToken = await signRefreshToken(email)
39 // res.status(200).send("Created user");
40 res.status(200).send({ accessToken, refreshToken });
41
42 }catch(error){
43 console.log(`register-Catch : ${error}`);
44 next(error);
45 }
46});

New learning from NodeJS perspective. next(), when its

  1. next() - call will be made to the next middleware in code.
  2. next(error) - if there is a parameter, it will be considered as error and code will try to execute the error handler.

Note - error-handling functions have four arguments instead of three: (err, req, res, next). More details here on error handling.

1//Error Handler - Should be the last one
2//500 - Internal server not found
3app.use((err, req, res, next) => {
4 res.status(err.status || 500)
5 res.send({
6 error: {
7 status: err.status || 500,
8 message: err.message,
9 },
10 })
11})
12
13//Listening Port
14app.listen(PORT, () => {
15 console.log(`Server running on port ${PORT}`)
16})

References