JSON Web Token defines a compact and self-contained way for securely transmitting information as a JSON object.
In the tutorial, we show how to build a Nodejs Token Authentication RestAPIs with JSON Web Token (JWT) and MongoDB.
Related posts:
– Mongoose Many-to-Many related models with NodeJS/Express, MongoDB
– Crud RestAPIs with NodeJS/Express, MongoDB using Mongoose
Technologies
– Nodejs/Express
– Json Web Token
– BCryptjs
– Mongoose
– MongoDB
JSON Web Token
JSON Web Token (JWT) defines a compact and self-contained way for securely transmitting information between parties as a JSON object.
-> Scenarios where JSON Web Tokens are useful:
- Authorization: the most common scenario for using JWT. Single Sign On is a feature that widely uses JWT
- Information Exchange: Because JWTs can be signed, JSON Web Tokens are a good way of securely transmitting information between parties.
JSON Web Tokens consist of 3 parts:
- Header
- Payload
- Signature
-> JWT
looks like Header-Base64-String.Payload-Base64-String.Signature-Base64-String
Header consists of two parts:
- token type.
- hashing algorithm.
-> Example:
{ "alg": "HS256", "typ": "JWT" }
Payload contains the claims. Claims are statements about an entity and additional information.
There are 3 types of claims ->
Registered claims
-> These are a set of predefined claims:iss
(issuer),exp
(expiration time),sub
(subject)Public claims
Private claims
Example ->
{ "id": 3, "iat": 1538339534, "exp": 1538425934 }
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.
Example ->
HMACSHA512( base64UrlEncode(header) + "." + base64UrlEncode(payload), your-256-bit-secret )
Combine all together, we get 3 Base64-URL strings separated by dots,
Example:
– Encoded ->
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MywiaWF0IjoxNTM4MzM5NTM0LCJleHAiOjE1Mzg0MjU5MzR9.wKse6-ERNP4g_sPBdM72GZgpNpHH87UGbzYH3_0mdpo
– Decoded ->
Overview
Project Structure
config
package defines MongoDB Database Configuration, JWT Secret Key & User Roles.model
package definesRole
&User
Mongoose models.router
package defines RestAPI URLs, verification functions for signup, & verification JWT token function for signin.controller
package defines proccesing functions for each RestAPIs declared inrouter
package.
Workflow
We will define 5 workflows as below ->
- SignUp Scenarios:
-> Verify UserName & Email -> If NOT Duplicate (UserName & Email), verify Roles are existed. -> If Roles are available, save User Info to database by Sequlieze ORM -> Othercase, Eror code will be returned
– Code in
router.js
->app.post('/api/auth/signup', [verifySignUp.checkDuplicateUserNameOrEmail, verifySignUp.checkRolesExisted], controller.signup);
- SignIn Scenarios:
-> Find User record in database by
username
-> If User is existed, checkpassword
is Valid or NOT -> If password is valid, create JWT then return JWT token back to client -> Othercase, Error code will be returned– Code in
router.js
->app.post('/api/auth/signin', controller.signin);
- Access User Content:
-> Verify JWT Token -> If token is valid,
controller
will load & return User Info back to client -> Othercase, Error Code will be returned– Code in
router.js
->app.get('/api/test/user', [authJwt.verifyToken], controller.userContent);
- Access PM Content:
-> Verify JWT Token -> If token is valid, verify
PM
role. -> If User has Admin or PM role,controller
will load & return Management Content to client. -> Othercase, Error code will be returned– Code in
router.js
->app.get('/api/test/pm', [authJwt.verifyToken, authJwt.isPmOrAdmin], controller.managementBoard);
- Access Admin Content
-> Verify JWT Token -> If token is valid, verify
ADMIN
role. -> If User has Admin role,controller
will load & return Admin Content to client. -> Othercase, Error code will be returned– Code in
router.js
->app.get('/api/test/admin', [authJwt.verifyToken, authJwt.isAdmin], controller.adminBoard);
Goal
Sign Up ->
Sign In ->
Access API Successfully ->
Unauthorized Access ->
Practice
Create Nodejs Project
Following the guide to create a NodeJS/Express project
Install Express, Mongoose, Json Web Token, Bcryptjs:
$npm install express mongoose jsonwebtoken bcryptjs --save
-> package.json
file:
{ "name": "nodejs-jwt-auth", "version": "1.0.0", "description": "Nodejs-JWT-Authentication-with-MongoDB-Mongoose-ORM", "main": "server.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [ "Nodejs", "Express", "JWT", "Mongoose", "MongoDB", "Authentication" ], "author": "ozenero.com", "license": "ISC", "dependencies": { "bcryptjs": "^2.4.3", "express": "^4.16.3", "jsonwebtoken": "^8.3.0", "mongoose": "^5.3.1", } }
Create Mongoose Models
– User
model ->
const Role = require('./role.model.js'); const mongoose = require('mongoose'), Schema = mongoose.Schema; const UserSchema = mongoose.Schema({ name: String, username: String, email: String, password: String, roles: [{ type: Schema.Types.ObjectId, ref: 'Role' }] }); module.exports = mongoose.model('User', UserSchema);
– Role
model:
const mongoose = require('mongoose'), Schema = mongoose.Schema; const RoleSchema = mongoose.Schema({ name: String }); module.exports = mongoose.model('Role', RoleSchema);
Project Configuration
– /app/config/config.js
file ->
module.exports = { 'secret': 'ozenero-super-secret-key', url: 'mongodb://localhost:27017/gkzdb', ROLEs: ['USER', 'ADMIN', 'PM'] };
Define RestAPIs Router
We define 5 RestAPIs in /app/router/router.js
const verifySignUp = require('./verifySignUp'); const authJwt = require('./verifyJwtToken'); module.exports = function(app) { const controller = require('../controller/controller.js'); app.post('/api/auth/signup', [verifySignUp.checkDuplicateUserNameOrEmail, verifySignUp.checkRolesExisted], controller.signup); app.post('/api/auth/signin', controller.signin); app.get('/api/test/user', [authJwt.verifyToken], controller.userContent); app.get('/api/test/pm', [authJwt.verifyToken, authJwt.isPmOrAdmin], controller.managementBoard); app.get('/api/test/admin', [authJwt.verifyToken, authJwt.isAdmin], controller.adminBoard); }
We need implement middleware functions to do a verification for SignUp
& SignIn
:
– /app/router/verifySignUp.js
implements 2 middleware functions:
checkDuplicateUserNameOrEmail
-> checking the postedusername
oremail
is duplicated or NOTcheckRolesExisted
-> checking the posted UserRole
is existed or NOT
const config = require('../config/config.js'); const ROLEs = config.ROLEs; const User = require('../model/user.model.js'); checkDuplicateUserNameOrEmail = (req, res, next) => { // -> Check Username is already in use User.findOne({ username: req.body.username }) .exec((err, user) => { if (err && err.kind !== 'ObjectId'){ res.status(500).send({ message: "Error retrieving User with Username = " + req.body.username }); return; } if(user){ res.status(400).send("Fail -> Username is already taken!"); return; } User.findOne({ email: req.body.email }) .exec((err, user) => { if (err && err.kind !== 'ObjectId'){ res.status(500).send({ message: "Error retrieving User with Email = " + req.body.email }); return; } if(user){ res.status(400).send("Fail -> Email is already in use!"); return; } next(); }); }); } checkRolesExisted = (req, res, next) => { for(let i=0; iDoes NOT exist Role = " + req.body.roles[i]); return; } } next(); } const signUpVerify = {}; signUpVerify.checkDuplicateUserNameOrEmail = checkDuplicateUserNameOrEmail; signUpVerify.checkRolesExisted = checkRolesExisted; module.exports = signUpVerify;
– /app/router/verifyJwtToken.js
implements 3 middleware functions:
verifyToken
-> checking a JWT token is valid or NOTisAdmin
-> checking an User hasADMIN
role or NOTisPmOrAdmin
-> checking an User hasPM
orADMIN
role or NOT
const jwt = require('jsonwebtoken'); const config = require('../config/config.js'); const User = require('../model/user.model.js'); const Role = require('../model/role.model.js'); verifyToken = (req, res, next) => { let token = req.headers['x-access-token']; if (!token){ return res.status(403).send({ auth: false, message: 'No token provided.' }); } jwt.verify(token, config.secret, (err, decoded) => { if (err){ return res.status(500).send({ auth: false, message: 'Fail to Authentication. Error -> ' + err }); } req.userId = decoded.id; next(); }); } isAdmin = (req, res, next) => { User.findOne({ _id: req.userId }) .exec((err, user) => { if (err){ if(err.kind === 'ObjectId') { return res.status(404).send({ message: "User not found with Username = " + req.body.username }); } return res.status(500).send({ message: "Error retrieving User with Username = " + req.body.username }); } Role.find({ '_id': { $in: user.roles } }, (err, roles) => { if(err) res.status(500).send("Error -> " + err); for(let i=0; i{ User.findOne({ _id: req.userId }) .exec((err, user) => { if (err){ if(err.kind === 'ObjectId') { return res.status(404).send({ message: "User not found with Username = " + req.body.username }); } return res.status(500).send({ message: "Error retrieving User with Username = " + req.body.username }); } Role.find({ '_id': { $in: user.roles } }, (err, roles) => { if(err) res.status(500).send("Error -> " + err); for(let i=0; i Implement Controller
-
/app/controller/controller.js
exports 5 funtions:
signup
-> be used to register new Usersignin
-> be used to LoginuserContent
-> get User InfomanagementBoard
-> get Management Board ContentadminBoard
-> get Admin Board Contentconst config = require('../config/config.js'); const Role = require('../model/role.model.js'); const User = require('../model/user.model.js'); var jwt = require('jsonwebtoken'); var bcrypt = require('bcryptjs'); exports.signup = (req, res) => { // Save User to Database console.log("Processing func -> SignUp"); const user = new User({ name: req.body.name, username: req.body.username, email: req.body.email, password: bcrypt.hashSync(req.body.password, 8) }); // Save a User to the MongoDB user.save().then(savedUser => { Role.find({ 'name': { $in: req.body.roles.map(role => role.toUpperCase()) } }, (err, roles) => { if(err) res.status(500).send("Error -> " + err); // update User with Roles savedUser.roles = roles.map(role => role._id); savedUser.save(function (err) { if (err) res.status(500).send("Error -> " + err); res.send("User registered successfully!"); }); }); }).catch(err => { res.status(500).send("Fail! Error -> " + err); }); } exports.signin = (req, res) => { console.log("Sign-In"); User.findOne({ username: req.body.username }) .exec((err, user) => { if (err){ if(err.kind === 'ObjectId') { return res.status(404).send({ message: "User not found with Username = " + req.body.username }); } return res.status(500).send({ message: "Error retrieving User with Username = " + req.body.username }); } var passwordIsValid = bcrypt.compareSync(req.body.password, user.password); if (!passwordIsValid) { return res.status(401).send({ auth: false, accessToken: null, reason: "Invalid Password!" }); } var token = jwt.sign({ id: user._id }, config.secret, { expiresIn: 86400 // expires in 24 hours }); res.status(200).send({ auth: true, accessToken: token }); }); } exports.userContent = (req, res) => { User.findOne({ _id: req.userId }) .select('-_id -__v -password') .populate('roles', '-_id -__v') .exec((err, user) => { if (err){ if(err.kind === 'ObjectId') { return res.status(404).send({ message: "User not found with _id = " + req.userId }); } return res.status(500).send({ message: "Error retrieving User with _id = " + req.userId }); } res.status(200).json({ "description": "User Content Page", "user": user }); }); } exports.adminBoard = (req, res) => { User.findOne({ _id: req.userId }) .select('-_id -__v -password') .populate('roles', '-_id -__v') .exec((err, user) => { if (err){ if(err.kind === 'ObjectId') { res.status(404).send({ message: "User not found with _id = " + req.userId }); return; } res.status(500).json({ "description": "Can not access Admin Board", "error": err }); return; } res.status(200).json({ "description": "Admin Board", "user": user }); }); } exports.managementBoard = (req, res) => { User.findOne({ _id: req.userId }) .select('-_id -__v -password') .populate('roles', '-_id -__v') .exec((err, user) => { if (err){ if(err.kind === 'ObjectId') { res.status(404).send({ message: "User not found with _id = " + req.userId }); return; } res.status(500).json({ "description": "Can not access PM Board", "error": err }); return; } res.status(200).json({ "description": "PM Board", "user": user }); }); }Server
-
/app/server.js
file ->var express = require('express'); var app = express(); var bodyParser = require('body-parser'); app.use(bodyParser.json()) require('./app/router/router.js')(app); const Role = require('./app/model/role.model.js'); // Configuring the database const config = require('./app/config/config.js'); const mongoose = require('mongoose'); mongoose.Promise = global.Promise; // Connecting to the database mongoose.connect(config.url) .then(() => { console.log("Successfully connected to MongoDB."); initial(); }).catch(err => { console.log('Could not connect to MongoDB.'); process.exit(); }); // Create a Server var server = app.listen(8080, function () { var host = server.address().address var port = server.address().port console.log("App listening at http://%s:%s", host, port) }) function initial(){ Role.count( (err, count) => { if(!err && count === 0) { // USER Role -> new Role({ name: 'USER' }).save( err => { if(err) return console.error(err.stack) console.log("USER_ROLE is added") }); // ADMIN Role -> new Role({ name: 'ADMIN' }).save( err => { if(err) return console.error(err.stack) console.log("ADMIN_ROLE is added") }); // PM Role -> new Role({ name: 'PM' }).save(err => { if(err) return console.error(err.stack) console.log("PM_ROLE is added") }); } }); }Run & Check Results
Start Nodejs Server
- Run Nodejs server by cmd
npm start
.-> Check MongoDB database:
Sign Up
-> MongoDB records:
SignIn and Access Protected Resources
- Adam can access
api/test/user
url, can NOT access others.-> Sign In:
-> Access Protected Resources:
- Jack can access
api/test/user
&api/test/pm
url.
Can NOT access/api/test/admin
url.-> Sign In:
-> Access Protected Resources:
– Thomas can access all URLs.
-> Access Protected Resource:
SourceCode