A comprehensive step by step tutorial on securing MEAN (MongoDB, Express.js, Angular 5, Node.js) Stack Web Application using Passport.js. Securing web application in this tutorial is to make a specific web page accessible only to the authorized user. That's the point of using Passport.js is to make authentication system for MEAN (Angular 5) stack web application. For the MEAN stack with Angular 5, you can find in the previous tutorial. Now, we add the passport.js module for authenticating the user. Don't worry, we always create a tutorial from scratch although we use the same steps as the previous tutorial.

Table of Contents:

We will use the REST API security approach for this tutorial because of it simple to do. Passport.js will handle authority that comes to REST API request then response the right Authentication code for the client (Angular 5).

The following tools, frameworks, and modules are required for this tutorial achievement:

Node.js MongoDB Angular 5 Express.js Passport.js Terminal or Command Line IDE or Text Editor

Before the move to the main steps of this tutorial, make sure that you have installed Node.js and MongoDB on your machine. You can check the Node.js version after installing it from the terminal or Node.js command line.

node -v

We use this version for this tutorial.

v8.9.4



Create Angular 5 Application using Angular CLI

As usual, we starting the tutorial from scratch which means start from zero. First, we have to install the Angular CLI. The Angular CLI is a tool to initialize, develop, scaffold and maintain Angular applications. Go to your Node project folder then type this command for installing the Angular-CLI.

sudo npm install -g @angular/cli

You can use `sudo` if using Mac/Linux terminal, it's not necessary when using the Windows command line. Next, type this command to create new Angular 5 application.

ng new mean-secure

That command will create a new Angular 5 app, also installing required `npm` modules. Go to the newly created application folder.

cd ./mean-secure

Run the Angular 5 application by typing this command.

ng serve

If you find the error below, install the module that shows in the error message.

Error: Cannot find module '@angular-devkit/core' npm install --save-dev @angular-devkit/core

Now, run again the Angular 5 application.

ng serve

Open the browser then go to this URL `localhost:4200` and you will see this Angular 5 landing page.



Combine Angular 5 with Express.js

Next, we will use Express.js as web and REST API server. After close the running Angular 5 application by press `CTRL+C`, type this command to install the required Express modules and dependencies.

npm install --save express body-parser morgan body-parser serve-favicon

Add a new folder named `bin` to the root of project folder and `www` file inside that folder. Actually, you are free to put or name starting file anywhere you like.

mkdir bin touch ./bin/www

Open and edit `./bin/www` the file then adds these lines of codes.

#!/usr/bin/env node /** * Module dependencies. */ var app = require('../app'); var debug = require('debug')('mean-secure:server'); var http = require('http'); /** * Get port from environment and store in Express. */ var port = normalizePort(process.env.PORT || '3000'); app.set('port', port); /** * Create HTTP server. */ var server = http.createServer(app); /** * Listen on provided port, on all network interfaces. */ server.listen(port); server.on('error', onError); server.on('listening', onListening); /** * Normalize a port into a number, string, or false. */ function normalizePort(val) { var port = parseInt(val, 10); if (isNaN(port)) { // named pipe return val; } if (port >= 0) { // port number return port; } return false; } /** * Event listener for HTTP server "error" event. */ function onError(error) { if (error.syscall !== 'listen') { throw error; } var bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port; // handle specific listen errors with friendly messages switch (error.code) { case 'EACCES': console.error(bind + ' requires elevated privileges'); process.exit(1); break; case 'EADDRINUSE': console.error(bind + ' is already in use'); process.exit(1); break; default: throw error; } } /** * Event listener for HTTP server "listening" event. */ function onListening() { var addr = server.address(); var bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port; debug('Listening on ' + bind); }

Next, we have to change the starting application to Express.js by open and edit `package.json` then change `start` value in `script` block.

"scripts": { "ng": "ng", "start": "ng build && node ./bin/www", "build": "ng build", "test": "ng test", "lint": "ng lint", "e2e": "ng e2e" },

Now, create app.js in the root of the project folder.

touch app.js

Open and edit app.js then add all these lines of codes.

var express = require('express'); var path = require('path'); var favicon = require('serve-favicon'); var logger = require('morgan'); var bodyParser = require('body-parser'); var api = require('./routes/api'); var app = express(); app.use(logger('dev')); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({'extended':'false'})); app.use(express.static(path.join(__dirname, 'dist'))); app.use('/', express.static(path.join(__dirname, 'dist'))); app.use('/api', api); // catch 404 and forward to error handler app.use(function(req, res, next) { var err = new Error('Not Found'); err.status = 404; next(err); }); // error handler app.use(function(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'); }); module.exports = app;

Next, create routes folder then create routes file for the profile REST API endpoint.

mkdir routes touch routes/api.js

Open and edit `routes/api.js` file then add this lines of codes.

var express = require('express'); var router = express.Router(); /* GET home page. */ router.get('/', function(req, res, next) { res.send('Express RESTful API'); }); module.exports = router;

Now, run the server using this command.

npm start

You will see the previous Angular landing page when you point your browser to `http://localhost:3000`. When you change the address to `http://localhost:3000/api/profile` you will see RESTful API response in the browser.



Setup Mongoose.js and Passport.js

We need to access data from MongoDB. For that, we will install and configure Mongoose.js. Mongoose is a MongoDB object modeling tool designed to work in an asynchronous environment. Passport is authentication middleware for Node.js. Extremely flexible and modular, Passport can be unobtrusively dropped into any Express-based web application. On the terminal type this command after stopping the running Express server.

npm install --save mongoose bluebird bcrypt-nodejs jsonwebtoken morgan passport passport-jwt

We will make separate files for the configuration. For that, create the new folder in the root folder.

mkdir config

Create a configuration file for Database and Passport.js.

touch config/database.js touch config/passport.js

Open and edit `config/database.js` then add this lines of codes.

module.exports = { 'secret':'meansecure', 'database': 'mongodb://localhost/mean-secure' };

This config holds the database connection parameter and secret for generating JWT token. Next, open and edit `config/passport.js` then add this lines of codes.

var JwtStrategy = require('passport-jwt').Strategy, ExtractJwt = require('passport-jwt').ExtractJwt; // load up the user model var User = require('../models/user'); var config = require('../config/database'); // get db config file module.exports = function(passport) { var opts = {}; opts.jwtFromRequest = ExtractJwt.fromAuthHeaderWithScheme("jwt"); opts.secretOrKey = config.secret; passport.use(new JwtStrategy(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); } }); })); };

This config is used for getting the user by matching JWT token with token get from the client. This configuration needs to create the User model later.

Now, Open and edit `app.js` then declare required library for initializing with the server by adding these lines of requires.

var morgan = require('morgan'); var mongoose = require('mongoose'); var passport = require('passport'); var config = require('./config/database');

Create a connection to MongoDB.

mongoose.Promise = require('bluebird'); mongoose.connect(config.database, { promiseLibrary: require('bluebird') }) .then(() => console.log('connection succesful')) .catch((err) => console.error(err));

Declare a variable for API route.

var api = require('./routes/api');

Initialize passport by add this line.

app.use(passport.initialize());

Add API route to the endpoint URL after other `use` function.

app.use('/api', api);



Create Mongoose.js Models or Schemas

We will create Mongoose models or schemas for the User and the Book collections. First, we have to create a folder for holds all model files. Create and name it `models` in the root of the project folder.

mkdir models

Create new Javascript file that uses for Mongoose.js model. We will create a model of User collection and Book collection.

touch models/User.js touch models/Book.js

Open and edit `models/User.js` then add these lines of codes.

var mongoose = require('mongoose'); var Schema = mongoose.Schema; var bcrypt = require('bcrypt-nodejs'); var UserSchema = new Schema({ username: { type: String, unique: true, required: true }, password: { type: String, required: true } }); 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); }); }; module.exports = mongoose.model('User', UserSchema);

On than schema, you will find the function of password encryption and password comparison for login purpose. We use `Bcrypt` library for password encryption. Next, open and edit "models/book.js" then add these lines of codes.

var mongoose = require('mongoose'); var Schema = mongoose.Schema; var BookSchema = new Schema({ isbn: { type: String, required: true }, title: { type: String, required: true }, author: { type: String, required: true }, publisher: { type: String, required: true } }); module.exports = mongoose.model('Book', BookSchema);



Create Express Routes

We will create a Router for authenticating the user and restrict book resources. In the Express routes folder creates a new Javascript file by type this command.

touch routes/api.js

Open and edit `routes/api.js` then declares all require variables.

var mongoose = require('mongoose'); var passport = require('passport'); var config = require('../config/database'); require('../config/passport')(passport); var express = require('express'); var jwt = require('jsonwebtoken'); var router = express.Router(); var User = require("../models/user"); var Book = require("../models/book");

Create a router for signup or register the new user.

router.post('/signup', function(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(function(err) { if (err) { return res.json({success: false, msg: 'Username already exists.'}); } res.json({success: true, msg: 'Successful created new user.'}); }); } });

Create router for login or sign-in.

router.post('/signin', function(req, res) { User.findOne({ username: req.body.username }, function(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, function (err, isMatch) { if (isMatch && !err) { // if user is found and password is right create a token var token = jwt.sign(user.toJSON(), config.secret); // 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.'}); } }); } }); });

Create router for add new book that only accessible to authorized user.

router.post('/book', passport.authenticate('jwt', { session: false}), function(req, res) { var token = getToken(req.headers); if (token) { console.log(req.body); var newBook = new Book({ isbn: req.body.isbn, title: req.body.title, author: req.body.author, publisher: req.body.publisher }); newBook.save(function(err) { if (err) { return res.json({success: false, msg: 'Save book failed.'}); } res.json({success: true, msg: 'Successful created new book.'}); }); } else { return res.status(403).send({success: false, msg: 'Unauthorized.'}); } });

Create a router for getting a list of books that accessible for an authorized user.

router.get('/book', passport.authenticate('jwt', { session: false}), function(req, res) { var token = getToken(req.headers); if (token) { Book.find(function (err, books) { if (err) return next(err); res.json(books); }); } else { return res.status(403).send({success: false, msg: 'Unauthorized.'}); } });

Create a function for parse authorization token from request headers.

getToken = function (headers) { if (headers && headers.authorization) { var parted = headers.authorization.split(' '); if (parted.length === 2) { return parted[1]; } else { return null; } } else { return null; } };

Finally, export router as a module.

module.exports = router;



Create Angular 5 Component for Login

To create the Angular 5 Component, simply run this command.

ng g component login

That command will generate all required files for build login component and also automatically added login component to app.module.ts.

create src/app/login/login.component.css (0 bytes) create src/app/login/login.component.html (24 bytes) create src/app/login/login.component.spec.ts (621 bytes) create src/app/login/login.component.ts (265 bytes) update src/app/app.module.ts (468 bytes)

Before adding any functionality to the component, we need to add `HttpClientModule` to `app.module.ts`. Open and edit `src/app/app.module.ts` then add this import.

import { FormsModule } from '@angular/forms'; import { HttpClientModule } from '@angular/common/http';

Add it to `@NgModule` imports after `BrowserModule`.

imports: [ BrowserModule, FormsModule, HttpClientModule ],

Now, we will post to login REST API using this Angular `HttpClient` module. Open and edit `src/app/login/login.component.ts` then add this import.

import { Component, OnInit } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Router } from "@angular/router"; import { Observable } from 'rxjs/Observable'; import { tap, catchError } from 'rxjs/operators'; import { of } from 'rxjs/observable/of';

Inject `HttpClient` and `Router` to the constructor.

constructor(private http: HttpClient, private router: Router) { }

Add object variable for holding login data, message and response data before the constructor.

loginData = { username:'', password:'' }; message = ''; data: any;

Add login function and a function for catch error response.

login() { this.http.post('/api/signin',this.loginData).subscribe(resp => { this.data = resp; localStorage.setItem('jwtToken', this.data.token); this.router.navigate(['books']); }, err => { this.message = err.error.msg; }); }

Next, open and edit `src/app/login/login.component.html` then replace all HTML tags with this.

<div class="container"> <form class="form-signin" (ngSubmit)="login()" #loginForm="ngForm"> <div class="alert alert-warning alert-dismissible" role="alert" *ngIf="message !== ''"> <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button> {{message}} </div> <h2 class="form-signin-heading">Please sign in</h2> <label for="inputEmail" class="sr-only">Email address</label> <input type="email" class="form-control" placeholder="Email address" [(ngModel)]="loginData.username" name="username" required/> <label for="inputPassword" class="sr-only">Password</label> <input type="password" class="form-control" placeholder="Password" [(ngModel)]="loginData.password" name="password" required/> <button class="btn btn-lg btn-primary btn-block" type="submit" [disabled]="!loginForm.form.valid">Sign in</button> <p> Not a member? <a [routerLink]="['/signup']">Signup here</a> </p> </form> </div>

Give a style to the login page. Open and edit `src/app/login/login.component.css` then add these styles.

body { padding-top: 40px; padding-bottom: 40px; background-color: #eee; } .form-signin { max-width: 330px; padding: 15px; margin: 0 auto; } .form-signin .form-signin-heading, .form-signin .checkbox { margin-bottom: 10px; } .form-signin .checkbox { font-weight: normal; } .form-signin .form-control { position: relative; height: auto; -webkit-box-sizing: border-box; box-sizing: border-box; padding: 10px; font-size: 16px; } .form-signin .form-control:focus { z-index: 2; } .form-signin input[type="email"] { margin-bottom: -1px; border-bottom-right-radius: 0; border-bottom-left-radius: 0; } .form-signin input[type="password"] { margin-bottom: 10px; border-top-left-radius: 0; border-top-right-radius: 0; } .form-signin p { margin-top: 10px; }

Above HTML tags include style class from Bootstrap CSS library. Open and edit `src/index.html` then add the Bootstrap CSS and JS library.

<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>MeanSecure</title> <base href="/"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/x-icon" href="favicon.ico"> <!-- Latest compiled and minified CSS --> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> <!-- Optional theme --> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous"> </head> <body> <app-root></app-root> <!-- Latest compiled and minified JavaScript --> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script> </body> </html>



Create Angular 5 Component for Signup

Same as the previous step, run this command to create a new Angular 5 component.

ng g component signup

Open and edit `src/app/signup/signup.component.ts` then add this import.

import { Component, OnInit } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Router } from "@angular/router"; import { Observable } from 'rxjs/Observable'; import { tap, catchError } from 'rxjs/operators'; import { of } from 'rxjs/observable/of';

Inject `HttpClient` and `Router` to the constructor.

constructor(private http: HttpClient, private router: Router) { }

Add object variable for holding login data and message before the constructor.

signupData = { username:'', password:'' }; message = '';

Add a function for sign up.

signup() { this.http.post('/api/signup',this.signupData).subscribe(resp => { console.log(resp); this.router.navigate(['login']); }, err => { this.message = err.error.msg; }); }

Next, open and edit `src/app/signup/signup.component.html` then replace all HTML tags with this.

<div class="container"> <form class="form-signin" (ngSubmit)="signup()" #signupForm="ngForm"> <div class="alert alert-warning alert-dismissible" role="alert" *ngIf="message !== ''"> <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button> {{message}} </div> <h2 class="form-signin-heading">Sign Up Now</h2> <label for="inputEmail" class="sr-only">Email address</label> <input type="email" class="form-control" placeholder="Email address" [(ngModel)]="signupData.username" name="username" required/> <label for="inputPassword" class="sr-only">Password</label> <input type="password" class="form-control" placeholder="Password" [(ngModel)]="signupData.password" name="password" required/> <button class="btn btn-lg btn-primary btn-block" type="submit" [disabled]="!signupForm.form.valid">Sign Up</button> </form> </div>

Give a style to the signup page. Open and edit `src/app/signup/signup.component.css` then add these styles.

body { padding-top: 40px; padding-bottom: 40px; background-color: #eee; } .form-signin { max-width: 330px; padding: 15px; margin: 0 auto; } .form-signin .form-signin-heading, .form-signin .checkbox { margin-bottom: 10px; } .form-signin .checkbox { font-weight: normal; } .form-signin .form-control { position: relative; height: auto; -webkit-box-sizing: border-box; box-sizing: border-box; padding: 10px; font-size: 16px; } .form-signin .form-control:focus { z-index: 2; } .form-signin input[type="email"] { margin-bottom: -1px; border-bottom-right-radius: 0; border-bottom-left-radius: 0; } .form-signin input[type="password"] { margin-bottom: 10px; border-top-left-radius: 0; border-top-right-radius: 0; } .form-signin p { margin-top: 10px; }



Create Angular 5 Component for Book

Same as the previous step, run this command to create a new Angular 5 component.

ng g component book

Open and edit `src/app/book/book.component.ts` then add this import.

import { Component, OnInit } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Router } from "@angular/router"; import { Observable } from 'rxjs/Observable'; import { tap, catchError } from 'rxjs/operators'; import { of } from 'rxjs/observable/of';

Inject `HttpClient` and `Router` to the constructor.

constructor(private http: HttpClient, private router: Router) { }

Add an array variable for holding books data before the constructor.

books: any;

Add a few lines of codes for getting a list of book data from RESTful API inside `ngOnInit` function.

ngOnInit() { let httpOptions = { headers: new HttpHeaders({ 'Authorization': localStorage.getItem('jwtToken') }) }; this.http.get('/api/book', httpOptions).subscribe(data => { this.books = data; console.log(this.books); }, err => { if(err.status === 401) { this.router.navigate(['login']); } }); }

Add a function for log out by simply delete JWT token.

logout() { localStorage.removeItem('jwtToken'); this.router.navigate(['login']); }

Now, we can display the book list on the page. Open and edit `src/app/book/book.component.html` then replace all tags with these lines of HTML tags.

<div class="container"> <h1>Book List <button class="btn btn-success" (click)="logout()">Logout</button></h1> <table class="table"> <thead> <tr> <th>ISBN</th> <th>Title</th> <th>Author</th> <th>Publisher</th> </tr> </thead> <tbody> <tr *ngFor="let book of books"> <td>{{ book.isbn }}</td> <td>{{ book.title }}</td> <td>{{ book.author }}</td> <td>{{ book.publisher }}</td> </tr> </tbody> </table> </div>



Create Angular 5 Router for All Pages

To make navigation working on Angular 5, we need to add the Router to the main module. Open and edit `src/app/app.module.ts` then add this import.

import { RouterModule, Routes } from '@angular/router';

Create a constant for routing before `@NgModule`.

const appRoutes: Routes = [ { path: 'books', component: BookComponent, data: { title: 'Book List' } }, { path: 'login', component: LoginComponent, data: { title: 'Login' } }, { path: 'signup', component: SignupComponent, data: { title: 'Sign Up' } }, { path: '', redirectTo: '/books', pathMatch: 'full' } ];

Add that router to `@NgModule` imports.

imports: [ BrowserModule, FormsModule, HttpClientModule, RouterModule.forRoot( appRoutes, { enableTracing: true } // <-- debugging purposes only ) ],

Next, open and edit `src/app/app.component.html` then replace all HTML tags with this.

<router-outlet></router-outlet>

To make `books` routing accessible via URL, open and edit `app.js` then change this line.

app.use('/', express.static(path.join(__dirname, 'dist')));

To

app.use('/books', express.static(path.join(__dirname, 'dist')));



Run and Test The MEAN (Angular 5) Stack Secure Web Application

Before running the MEAN stack application, run MongoDB server in another terminal tab.

mongod

Now, run the application.

npm start

Now, open the browser then go to this URL `localhost:3000`. You will be redirected to the login page.

Next, signup with email and password. After login successful, you will be entering the Book List page.

That it's for now. You can get the full source code for comparison with this tutorial on our GitHub.

That just the basic. If you need more deep learning about MEAN Stack, Angular, and Node.js, you can find the following books:

For more detailed on MEAN stack and Node.js, you can take the following course:

Thanks!