This tutorial explains how to implement REST API and Token based authentication in Node.js, Express, Mongoose environment. We are going to use JWT (JSON Web Token) + bcrypt (password hashing algo)+ Passport (authentication middleware to integrate different login strategies) combination. So jsonwebtoken, bcrypt-nodejs and passport-jwt javascript libraries will be used. It is assumed you are familiar with Node.js, MongoDB and ES6 basics.
Environment:
Node 8.11.3
NPM 5.6.0
mongoose 5.2.x
express-generator 4.16.0
express 4.16.0
passport 0.4.0
passport-jwt 4.0.0
jsonwebtoken 8.3.0
bcrypt-nodejs 0.0.3
babel-cli 6.26.3
babel-preset-es2015 6.24.1
Setup New Express Application
The easiest way to setup express application is to use express generator. Let's run following command
npm install express-generator -g
express rest-auth-sample
It will create rest-auth-sample folder and generate express application in it. Let's install the package and run the application by following commands:
cd rest-auth-sample
npm install
npm start
Now on browser, http://localhost:3000 will show the default express page.
Install Packages
run the following command to install the packages:
npm install mongoose bcrypt-nodejs jsonwebtoken passport passport-jwt --save
We are going to use ES6 syntaxes so need babel, run the following commands:
npm install babel-cli babel-preset-es2015 babel-plugin-add-module-exports shx --save-dev
Setup Configuration
Add config/db.js file with following:
export default {
'secret':'my secret',
'database': process.env.NODE_ENV == 'test' ? 'mongodb://localhost/rest-api-test' : 'mongodb://localhost/rest-api'
};
Models
We are going to create two models - user and project.
Create Models folder and add user.model.js
import mongoose from 'mongoose';
import bcrypt from 'bcrypt-nodejs';
const Schema = mongoose.Schema;
mongoose.set('useCreateIndex', true);
var UserSchema = new Schema({
username: {
type: String,
unique: true,
required: true
},
password: {
type: String,
required: true
},
},
{
timestamps: { createdAt: "created_at", updatedAt: "updated_at" }
});
UserSchema.pre('save', function (next) {
var user = this;
if (this.isModified('password') || this.isNew) {
bcrypt.genSalt(10, function (err, salt) {
if (err) {
return next(err);
}
bcrypt.hash(user.password, salt, null, function (err, hash) {
if (err) {
return next(err);
}
user.password = hash;
next();
});
});
} else {
return next();
}
});
UserSchema.methods.comparePassword = function (passw, cb) {
bcrypt.compare(passw, this.password, function (err, isMatch) {
if (err) {
return cb(err);
}
cb(null, isMatch);
});
};
export default mongoose.model('User', UserSchema);
bcrypt is used for password hash. Similarly add following for Models/project.model.js
import mongoose from 'mongoose';
const Schema = mongoose.Schema;
const ProjectSchema = new Schema({
title: {
type: String,
required: true
},
summary: {
type: String,
required: true
},
description: {
type: String,
required: true
},
submitDate: {
type: Date,
required: true
},
submittedBy:{
type: String,
required: true
}
});
const model = mongoose.model('Project', ProjectSchema);
export const cleanCollection = () => model.deleteMany({}).exec();
export default model;
Controllers
Add a controllers/auth.controller.js file and use following code:
import config from '../config/db';
import User from "../models/user.model";
const jwt = require('jsonwebtoken');
class Auth {
signUp(req, res) {
if (!req.body.username || !req.body.password) {
res.json({success: false, msg: 'Please pass username and password.'});
} else {
var newUser = new User({
username: req.body.username,
password: req.body.password
});
// save the user
newUser.save((err) => {
if (err) {
return res.json({success: false, msg: 'Username already exists.'});
}
res.json({success: true, msg: 'Successful created new user.'});
});
}
}
signIn(req, res) {
User.findOne({
username: req.body.username
}, (err, user) => {
if (err) throw err;
if (!user) {
res.status(401).send({success: false, msg: 'Authentication failed. User not found.'});
} else {
// check if password matches
user.comparePassword(req.body.password, (err, isMatch) => {
if (isMatch && !err) {
// if user is found and password is right create a token
var token = jwt.sign(user.toJSON(), config.secret,{ expiresIn: '30m' });
// return the information including token as JSON
res.json({success: true, token: 'JWT ' + token});
} else {
res.status(401).send({success: false, msg: 'Authentication failed. Wrong password.'});
}
});
}
});
}
}
export default new Auth();
Similarly add controllers/project.controller.js file
import Project from "../models/project.model";
class ProjectController{
async add(req, res) {
if (req.user && req.user.username) {
const newProject = new Project({
title: req.body.title,
summary: req.body.summary,
description: req.body.description,
submitDate: req.body.submitDate,
submittedBy: req.user.username
});
try{
let result = await newProject.save();
res.json({success: true, msg: 'New project is created successfully.'});
}
catch(err){
return res.json({success: false, msg: 'Save project failed.'});
}
} else {
return res.status(403).send({success: false, msg: 'Unauthorized.'});
}
}
async get(req, res) {
if (req.user && req.user.username) {
try{
let projects = await Project.find({submittedBy: req.user.username}).lean().exec();
return res.json(projects);
}
catch(err){
return next(err);
}
} else {
return res.status(403).send({success: false, msg: 'Unauthorized.'});
}
}
}
export default new ProjectController();
Setup Passport
In config folder, add file passport.js with following code
import passport from 'passport';
import { Strategy, ExtractJwt } from "passport-jwt";
import User from '../models/user.model';
import config from '../config/db'; // get db config file
class passportManager {
initialize(){
var opts = {
jwtFromRequest : ExtractJwt.fromAuthHeaderWithScheme("jwt"),
secretOrKey : config.secret
}
passport.use(new Strategy(opts, function(jwt_payload, done) {
User.findOne({id: jwt_payload.id}, function(err, user) {
if (err) {
return done(err, false);
}
if (user) {
done(null, user);
} else {
done(null, false);
}
});
}));
return passport.initialize();
}
authenticate(req, res, next){
passport.authenticate('jwt', { session: false}, (err, user, info) => {
if (err) { return next(err); }
if (!user) {
if (info.name === "TokenExpiredError") {
return res.status(401).json({ message: "Your token has expired." });
} else {
return res.status(401).json({ message: info.message });
}
}
req.user = user;
return next();
})(req, res, next);
};
}
export default new passportManager();
Setup Routers
Express generators generates two files in routes folder index.js and users.js. Rename to index.route.js and users.route.js for better naming conventions.
Add auth.route.js file:
import authController from "../controllers/auth.controller";
import express from 'express';
const router = express.Router();
router.post('/signup', authController.signUp);
router.post('/signin', authController.signIn);
export default router;
Add project.route.js file:
import express from 'express';
import projectController from "../controllers/project.controller";
import passportManager from '../config/passport';
const router = express.Router();
router.route('/')
.get(passportManager.authenticate, projectController.get)
.post(passportManager.authenticate, projectController.add);
export default router;
As project routes must be accessed by authenticated users so authenticate middleware is called before controller method.
Let's add index.js file in routes folder to manage all routes file
import express from 'express';
import indexRouter from './index.route';
import usersRouter from './users.route';
import authRouter from './auth.route';
import projectRouter from './project.route';
const router = express.Router();
router.use('/', indexRouter);
router.use('/users', usersRouter);
router.use('/api/auth', authRouter);
router.use('/api/project', projectRouter);
export default router;
app.js
Update app.js generated by express generator with following:
import createError from 'http-errors';
import express from 'express';
import path from 'path';
import cookieParser from 'cookie-parser';
import logger from 'morgan';
import mongoose from 'mongoose';
import passportManager from './config/passport';
import config from './config/db';
import router from './routes';
const app = express();
//mongoose setup
mongoose.Promise = Promise;
mongoose.connect(config.database, { useNewUrlParser: true });
mongoose.connection.on('error', () => {
throw new Error(`unable to connect to database: ${mongoUri}`);
});
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
// routes setup
app.use('/',router);
// catch 404 and forward to error handler
app.use((req, res, next) =>{
next(createError(404));
});
// error handler
app.use((err, req, res, next) =>{
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
app.use((req, res, next) =>{
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});
app.use(passportManager.initialize());
export default app;
Babel
As we are using ES6 so need to consider backwards compatible version of JavaScript in current and older browsers or environments. Thanks Babel to help us there. Babel is a transpiler (translates code in one language to another computer language at the same abstraction level) that can turn our ES6 code into ES5. We have already installed the related packages. Let's configure it.
Add .babelrc file and use following configuration
{
"presets": ["es2015"],
"plugins": [
"add-module-exports"
]
}
In package.json, use following scripts:
"scripts": {
"clean": "shx rm -rf dist",
"copy": "shx mkdir dist && shx cp -r public views dist/",
"compile": "npm run clean && npm run copy && babel \"./{,!(dist|node_modules)/**/}*.js\" bin/www -d dist",
"start": "npm run compile && node ./dist/bin/www"
}
Shx provides an easy way to execute simple Unix-like, cross-platform commands in npm package scripts. Basically ES6 code is converted and put into dist folder and public client side files/folders are also copied in dist folder. dist folder is used to execute the application.
Let's run the app by following command and check REST APIs
npm run start
I am using Postman (Chrome extension) to test REST APIs.
To create new user:
Let's check login api:
On successful login, JWT token is returned. I will use it to test project APIs
Let's create a new project:
In the header, I am passing JWT token
To get list of projects:
If you try to use API without authorization header, you will get following response:
{
"message": "No auth token"
}
For Invalid token, you will get following response:
{
"message": "jwt malformed"
}
Source Code
The full source code is available on GitHub.
Conclusion
This tutorial covers:
- How to setup express based application
- How to develop REST APIs
- How to implement token based authentication using Passport, JWT and bcrypt
- How to configure ES6 application with Babel
- How to test REST APIs with Postman
If you have any suggestion or question, feel free to leave a comment below.
Enjoy Node.js!