— notes, nodejs, security — 3 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.
First, just know there are two types of tokens,
In the below approach,
Packages beings used are
1npm install --save express morgan http-errors dotenv jsonwebtoken nodemon bcrypt
Below program is run on two scenarios
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## Output8D:\BigData\14.Nodejs\16.JWT\helpers>node generate-token.js9┌─────────┬────────────────────────────────────────────────────────────────────┐10│ (index) │ Values │11├─────────┼────────────────────────────────────────────────────────────────────┤12│ key1 │ '8681e735b3030348abc773be0ff17e04659d6c731a5e55ea3c54207c527ca4d6' │13│ key2 │ '2b51670f780250f02419ede7eaf55232c167509842cb12d5493b57e9ae3d8c5f' │14└─────────┴────────────────────────────────────────────────────────────────────┘
Below are the contents of .env file
1PORT=30002ACCESS_TOKEN_SECRET=8681e735b3030348abc773be0ff17e04659d6c731a5e55ea3c54207c527ca4d63REFRESH_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.
1...2let signAccessToken = (userId) => {3 return new Promise((resolve, reject) => {4 const payload = {}5 const secret = process.env.ACCESS_TOKEN_SECRET6 // 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 return17 }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.
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.
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.
There are 3 types of error codes,
TokenExpiredError
JsonWebTokenError
NotBeforeError
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.message13 return next(createError.Unauthorized(message))14 }15 req.payload = payload16 next()17 })18}19...
In the below test code, the refresh and access tokens are just retured back to the client
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 not9 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
users.json
file is just read, here it is read and does few things extra1router.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 not11 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" logic25 // data[email] = password; 26
27 //Encrypting Password28 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
next()
- call will be made to the next middleware in code. 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 one2//500 - Internal server not found3app.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 Port14app.listen(PORT, () => {15 console.log(`Server running on port ${PORT}`)16})