Chapter 4: The MEAN Stack Security
Activity 11: Securing the RESTful API
File name: userController.js
Live link: http://bit.ly/2DJEs5S
Create an admin user model by creating a file named userModel.js inside the api folder and input the following code:
const mongoose = require("mongoose"), // loading modules bcrypt = require('bcryptjs'), Schema = mongoose.Schema; const UserSchema = new Schema({ // Schema Instance fullName: { type: String, trim: true, required: true }, email: { type:String, unique:true, lovercase:true, trim:true, required:true } , hash_password: { type:String, required:true }, createdOn: { type: Date, default: Date.now } }); UserSchema.methods.comparePassword = function(password){ //password confirmation return bcrypt.compareSync(password, this.hash_password); } module.exports = mongoose.model("User", UserSchema); //user model
Create an admin user controller by creating a file called userController.js inside the controllers/api folder input using the following code:
const User = require("../models/userModel"); // Import userModel, jwt = require('jsonwebtoken'), // load jasonwebtoken module bcrypt = require('bcryptjs'); // load bcryptjs module for password hashing exports.register = (req, res) => { // exportable function to register new user let newUser = new User(req.body); newUser.hash_password = bcrypt.hashSync(req.body.password, 10); newUser.save((err, user) => { if (err) { res.status(500).send({ message: err }); } user.hash_password = undefined; res.status(201).json(user); }); }; //[…] exports.loginRequired = (req, res, next) => { if (req.user) { res.json({ message: 'Authorized User!'}); next(); } else { res.status(401).json({ message: 'Unauthorized user!' }); } };
Update the route file (articleListRoutes.js) in the routes directory (server/api/controllers) with the following code:
'use strict'; module.exports = function(app) { var articleList = require('../controllers/articleListController'); var userHandlers = require('../controllers/userController'); // articleList Routes app .route("/articles") .get(articleList.listAllArticles) .post(userHandlers.loginRequired, articleList.createNewArticle); app .route("/article/:articleid") .get(articleList.readArticle) .put(articleList.updateArticle) .delete(articleList.deleteArticle); app .route("/articles/by/:tag") .get(articleList.listTagArticles); app .route("/auth/register") .post(userHandlers.register); app .route("/auth/sign_in") .post(userHandlers.signIn); };
Update the server.js (inside the server folder) file with the following code:
'use strict' const express = require("express"); const bodyParser = require("body-parser"); // db instance connection require("./config/db"); var User = require('./api/models/userModel'), jsonwebtoken = require("jsonwebtoken"); const app = express(); const port = process.env.PORT || 3000; app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); //CORS (Cross-Origin Resource Sharing) headers to support Cross-site HTTP requests app.use(function(req, res, next) { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,PATCH,OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Length, X-Requested-With'); // allow preflight if (req.method === 'OPTIONS') { res.send(200); } else { next(); } }); app.use((req, res, next) => { // Verify JWT for user authorization if (req.headers && req.headers.authorization && req.headers.authorization.split(' ')[0] === 'JWT') { jsonwebtoken.verify(req.headers.authorization.split(' ')[1], 'RESTfulAPIs', (err, decode) => { if (err) req.user = undefined; req.user = decode; next(); }); } else { req.user = undefined; next(); } }); // API ENDPOINTS var routes = require('./api/routes/articleListRoutes'); //importing route routes(app); // LISTENING app.listen(port, () => { console.log('Server running at http://localhost:${port}'); });
Run the server using node server on the CLI and open Postman for testing.
Test for registration by typing in localhost:3000/auth/register on the address bar. You will obtain the following output:
Attempt the login required path and post request on localhost:3000/articles. You will obtain the following output:
Attempt user login by typing in localhost:3000/auth/sign_in on the address bar. You will obtain the following output:
Set the authentication key on the header and input the value in JWT token format, as shown here:
Attempt the login required path and post request on localhost:3000/articles. You will obtain the following output:
Thus, from the preceding outputs, it can be clearly observed that we have successfully secured the RESTful API we developed in the previous exercise. We also managed to provide admin access for creating, updating, and deleting data.
Activity 12: Creating a Login Page to Allow Authentication with Twitter Using Passport Strategies
File name: passport.js
Live link: http://bit.ly/2FXYfki
Create a package.json file and install express, body-parser, mongoose, passport-twitter, and passport by running the following code:
npm init npm install express body-parser mongoose passport-twitter passport express-session -save
Create a server.js (using touch server.js on the CLI) file and import express and body-parser using the following code:
const express = require("express"); const bodyParser = require("body-parser"); const session = require('express-session');
Create an Express application, assign a port number, and use the body-parser middleware on the Express application using the following code:
const app = express(); const port = process.env.PORT || 4000; app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json());
Create a config folder using the following code:
mkdir config
Create a database by first creating a file named db.js (using touch db.js on the CLI) in the config folder directory and input the following code:
const mongoose = require("mongoose"); var uri = "mongodb+srv://username:[email protected]/test?retryWrites=true"; const options = { reconnectTries: Number.MAX_VALUE, poolSize: 10 }; // Connect to the database using the following code mongoose.connect(uri, options).then( () => { console.log("Database connection established!"); }, err => { console.log("Error connecting Database instance due to: ", err); } );
Create an api directory and three subfolders named controllers, models, and routes using the following code:
mkdir api mkdir – controllers && mkdir – models && mkdir – routes
Create a model file inside the controller directory (using touch userModel.js on the CLI) and then create the schema and model:
const mongoose = require("mongoose"), const Schema = mongoose.Schema; const UserSchema = new Schema({ twitter : { fullName : String, email : String, createdOn: { type: Date, default: Date.now }, }, }); //Create a mongoose model from the Schema mongoose.model("User", UserSchema);
Create a passport file inside the config directory (using touch passport.js) and then create a Twitter strategy using the following code:
const TwitterStrategy = require("passport-twitter").Strategy; const mongoose = require('mongoose'), const User = mongoose.model('User'); // Create an exposed function module.exports = function (passport) {} serialize the user for the session passport.serializeUser(function (user, done) { done(null, user.id); }); deserialize the user passport.deserializeUser(function (id, done) { User.findById(id, function (err, user) { done(err, user); }); }); //[…] } else { done(null, user); } }); } )); }
Create a route file inside the routes directory (using touch route.js) and then create an exposed function as an exportable module that takes in app and passport using the following code:
module.exports = function(app,passport) { app.get('/auth/twitter', passport.authenticate(' twitter ', {scope:"email"})); app.get('/auth/ twitter /callback', passport.authenticate('twitter', { failureRedirect: '/error' }), function(req, res) { res.redirect('/success'); }); }
Update the server.js file with the following code:
//db instance connection require("./config/db"); app.get('/success', (req, res) => res.send("You have successfully logged in")); app.get('/error', (req, res) => res.send("error logging in"));
Import and initialize passport using the following code:
const passport = require("passport"); // Authentication configuration app.use(session({ resave: false, saveUninitialized: true, secret: 'bla bla bla' })) app.use(passport.initialize()); app.use(passport.session());
Update the API endpoint using the following code:
var routes = require('./api/routes/route'); //importing route routes(app,passport);app.listen(port, () => { console.log('Server running at http://localhost:${port}'); });
Run the server (using node server on the CLI), open a browser, and test this by typing in localhost:4000/auth/twitter on the browser address bar. You will obtain the following output: