Angular 10 + Nodejs JWT Token Based Authentication with MySQL Example – Express RestAPIs + JWT + BCryptjs + Sequelize

Angular 10 & Nodejs Security JWT Authentication MySQL Example

“How to implement Angular 10 + Nodejs JWT Token Based Authentication with MySQL Example?” is always a big common question in development world? So in the tutorial, I guide you very clearly how to build a full stack example to demonstrate an JWT Authentication flow from Angular (signup & signin with by JWT Token) to backend: Nodejs and database MySQL.

Related posts:


Now let’s go!

Introduction to Overview – Angular 10 + Nodejs JWT Token Based Authentication with MySQL Example

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:

{
  "sub": "thomasgkz",
  "iat": 1537603195,
  "exp": 1537689595
}

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:

eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0aG9tYXNna3oiLCJpYXQiOjE1Mzc2MDMxOTUsImV4cCI6MTUzNzY4OTU5NX0.m2YMjTYmOnfR7nnVNxqCzWbQ2FhKRe1eiizxnC2TF4eAoEzKlwo7PheVkKcxj08ST3vB-ZOIhiORvYVfSgzcog

When accessing a protected route or resource, the user agent should send the JWT, typically in the Authorization header using the Bearer schema.

Example:

Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0aG9tYXNna3oiLCJpYXQiOjE1Mzc2MDMxOTUsImV4cCI6MTUzNzY4OTU5NX0.m2YMjTYmOnfR7nnVNxqCzWbQ2FhKRe1eiizxnC2TF4eAoEzKlwo7PheVkKcxj08ST3vB-ZOIhiORvYVfSgzcog

Angular 10 Nodejs/Express JWT token based Authentication example

We will build an application, from frontend (Angular) to backend (Nodejs/Express), which allows users to register, login account. This application is secured with JWT (JSON Web Token) authentication and Nodejs middleware security. Then, depending on the role of current User (user, pm or admin), this system accepts what he can access:

Angular Register-Form
Angular Register-Form
Angular-Login-Form
Angular-Login-Form
Content-of-Jack-User
Content-of-Jack-User
Home Page of an User
Home Page of an User

The diagram below show how our system handles User Registration and User Login processes:

Angular Nodejs Security Jwt Authentication Work Process Diagram
Angular Nodejs Security Jwt Authentication Work Process Diagram

Full Stack Architecture

Nodejs/Express back-end with Middleware Security

Angular Nodejs Jwt Authentication Architecture Diagram Back End Server
Angular Nodejs Jwt Authentication Architecture Diagram Back End Server

HTTP request that matches route will be accepted by CORS Middleware before coming to Security layer.

Security layer includes:
JWT Authentication Middleware: verify SignUp, verify token
Authorization Middleware: check User’s roles

Main Business Logic Processing interacts with database via Sequelize and send HTTP response (token, user information, data based on roles…) to client.

Angular front-end with Interceptor

Angular Nodejs Jwt Authentication Architecture Diagram Front End Client
Angular Nodejs Jwt Authentication Architecture Diagram Front End Client

app.component is the parent component that contains routerLink and router-outlet for routing. It also has an authority variable as the condition for displaying items on navigation bar.
user.component, pm.component, admin.component correspond to Angular Components for User Board, PM Board, Admin Board. Each Board uses user.service to access authority data.
register.component contains User Registration form, submission of the form will call auth.service.
login.component contains User Login form, submission of the form will call auth.service and token-storage.service.

user.service gets access to authority data from Server using Angular HttpClient ($http service).
auth.service handles authentication and signup actions with Server using Angular HttpClient ($http service).
– every HTTP request by $http service will be inspected and transformed before being sent to the Server by auth-interceptor (implements HttpInterceptor).
auth-interceptor check and get Token from token-storage.service to add the Token to HTTP Request Header.

token-storage.service manages Token inside Browser’s sessionStorage.

Video Guide – Angular 10 + Nodejs JWT Token based Authentication with MySQL Example

Build Backend Nodejs JWT Security RestAPIs

JWT Authentication with Nodejs/Express RestAPIs

Overview

Look back the Node.js JWT Authentication Backend Diagram:

Angular Nodejs Jwt Authentication Architecture Diagram Back End Server
Angular Nodejs Jwt Authentication Architecture Diagram Back End Server

More details, see again the session: Nodejs/Express back-end with Middleware Security

Config Middleware & RestAPIs

module.exports = function (app) {
 
  const controller = require('../controller/controller.js');
 
  app.use(function (req, res, next) {
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Headers", "x-access-token, Origin, Content-Type, Accept");
    next();
  });
 
  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);
}

– For HTTP Header, we allow x-access-token for JWT.
– When a HTTP request call /signup api, it will also be passed to checkDuplicateUserNameOrEmail() and checkRolesExisted() funtions before going to controller’s signup() funtion.
– JWT Authentication middleware with verifyToken() and role checking funtions (isPmOrAdmin, isAdmin) will be called before controller returns authorized data (based on roles).

Generate Token

Inside controller’s signin() funtion, we use sign() funtion from jsonwebtoken:

var jwt = require('jsonwebtoken');
 
exports.signin = (req, res) => {
  User.findOne({
    where: { username: req.body.username }
  }).then(user => {
    // check user & password...
 
    var token = jwt.sign({ id: user.id }, config.secret, {
      expiresIn: 86400 // expires in 24 hours
    });
 
    // get other user information
    res.status(200).send({
      auth: true,
      accessToken: token,
      username: user.username,
      authorities: authorities
    });
  });
}

Verify Token

We get token from x-access-token of HTTP headers, then use verify() function of jsonwebtoken:

const jwt = require('jsonwebtoken');
 
verifyToken = (req, res, next) => {
  let token = req.headers['x-access-token'];
  
  if (!token){ // notice that no token was provided...}
 
  jwt.verify(token, 'SECRET KEY', (err, decoded) => {
    if (err){
      return res.status(500).send({ 
          auth: false, 
          message: 'Fail to Authentication. Error -> ' + err 
        });
    }
    req.userId = decoded.id;
    next();
  });
}

User & Roles Relationship model

We define Role & User Sequelize models as below:

Angular Nodejs Jwt Authentication Tutorial Back End Sequelize Many-to-many User Role
Angular Nodejs Jwt Authentication Tutorial Back End Sequelize Many-to-many User Role

Implementation of the Many-to-Many relationship:

// user.model.js
module.exports = (sequelize, Sequelize) => {
  const User = sequelize.define('users', {
    name: {
      type: Sequelize.STRING
    },
    username: {
      type: Sequelize.STRING
    },
    email: {
      type: Sequelize.STRING
    },
    password: {
      type: Sequelize.STRING
    }
  });
  
  return User;
}
 
// role.model.js
module.exports = (sequelize, Sequelize) => {
  const Role = sequelize.define('roles', {
    id: {
        type: Sequelize.INTEGER,
        primaryKey: true
    },
    name: {
      type: Sequelize.STRING
    }
  });
  
  return Role;
}
 
// db.config.js
const Sequelize = require('sequelize');
const sequelize = new Sequelize(...);
 
const db = {};
 
db.Sequelize = Sequelize;
db.sequelize = sequelize;
 
db.user = require('../model/user.model.js')(sequelize, Sequelize);
db.role = require('../model/role.model.js')(sequelize, Sequelize);
 
db.role.belongsToMany(db.user, { through: 'user_roles', foreignKey: 'roleId', otherKey: 'userId'});
db.user.belongsToMany(db.role, { through: 'user_roles', foreignKey: 'userId', otherKey: 'roleId'});
 
module.exports = db;

Overview Nodejs server for JWT Authentication

Goal

The diagram below show how our system handles User Registration and User Login processes:

Angular Nodejs Security Jwt Authentication Work Process Diagram
Angular Nodejs Security Jwt Authentication Work Process Diagram

– SignUp /api/auth/signup:

Nodejs JWT Authentication SignUp - Jack user with PM role
Nodejs JWT Authentication SignUp – Jack user with PM role

– Sign In /api/auth/signin:

SignIn Jack User
SignIn Jack User

– Access User Page /api/test/user:

Access to protected page
Access to protected page

– Access PM Page /api/test/pm:

Access to PM page
Access to PM page

– Access to Admin page /api/test/admin:

Jack can NOT access Admin Page
Jack can NOT access Admin Page

Technologies

– Nodejs/Express
– Json Web Token
– BCryptjs
– Sequelize
– MySQL

Project Structure

Nodejs JWT authentication project structure
Nodejs JWT authentication project structure

config package defines MySQL Database Configuration, JWT Secret Key & User Roles.
model package defines Role & User Sequelize models.
router package defines RestAPI URLs, verification functions for signup api, JWT verification for signin api, and authorization functions for content requested by user roles.
controller package defines process functions for each RestAPIs declared in router package.

Implementation – How to code it?

Create Nodejs Project

Install Express, Sequelize, MySQL, Json Web Token, Bcryptjs:

$npm install express sequelize mysql2 jsonwebtoken bcryptjs --save

package.json

{
  "name": "nodejs-jwt-authentication",
  "version": "1.0.0",
  "description": "Nodejs JWT Authentication project",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "Nodejs",
    "JWT",
    "Authentication",
    "Express",
    "RestAPIs"
  ],
  "author": "https://loizenai.com",
  "license": "ISC",
  "dependencies": {
    "bcryptjs": "^2.4.3",
    "express": "^4.17.1",
    "jsonwebtoken": "^8.5.1",
    "mysql2": "^2.1.0",
    "sequelize": "^5.21.12"
  }
}

Create Sequelize Models

User

model/user.model.js

module.exports = (sequelize, Sequelize) => {
  const User = sequelize.define('users', {
    name: {
      type: Sequelize.STRING
    },
    username: {
      type: Sequelize.STRING
    },
    email: {
      type: Sequelize.STRING
    },
    password: {
      type: Sequelize.STRING
    }
  });
  
  return User;
}
Role

model/role.model.js

module.exports = (sequelize, Sequelize) => {
  const Role = sequelize.define('roles', {
    id: {
        type: Sequelize.INTEGER,
        primaryKey: true
    },
    name: {
      type: Sequelize.STRING
    }
  });
  
  return Role;
}

Sequelize Database Configuration

config/env.js

For MySQL database:

 
const env = {
  database: 'loizenaidb',
  username: 'root',
  password: '12345',
  host: 'localhost',
  dialect: 'mysql',
  pool: {
    max: 5,
    min: 0,
    acquire: 30000,
    idle: 10000
  }
};
 
module.exports = env;

For PostgreSQL database:

const env = {
  database: 'loizenai',
  username: 'postgres',
  password: '123',
  host: 'localhost',
  dialect: 'postgres',
  pool: {
    max: 5,
    min: 0,
    acquire: 30000,
    idle: 10000
  }
};
 
module.exports = env;

Define RestAPIs Router with Middleware

RestAPIs Router

router/router.js

const verifySignUp = require('./verifySignUp');
const authJwt = require('./verifyJwtToken');
 
module.exports = function (app) {
 
  const controller = require('../controller/controller.js');
 
  app.use(function (req, res, next) {
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Headers", "x-access-token, Origin, Content-Type, Accept");
    next();
  });
 
  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);
}
Middleware functions

router/verifySignUp.js

const db = require('../config/db.config.js');
const config = require('../config/config.js');
const ROLEs = config.ROLEs;
const User = db.user;
 
checkDuplicateUserNameOrEmail = (req, res, next) => {
  // -> Check Username is already in use
  User.findOne({
    where: {
      username: req.body.username
    }
  }).then(user => {
    if (user) {
      res.status(400).send("Fail -> Username is already taken!");
      return;
    }
 
    // -> Check Email is already in use
    User.findOne({
      where: {
        email: req.body.email
      }
    }).then(user => {
      if (user) {
        res.status(400).send("Fail -> Email is already in use!");
        return;
      }
 
      next();
    });
  });
}
 
checkRolesExisted = (req, res, next) => {
  for (let i = 0; i < req.body.roles.length; i++) {
    if (!ROLEs.includes(req.body.roles[i].toUpperCase())) {
      res.status(400).send("Fail -> Does NOT exist Role = " + req.body.roles[i]);
      return;
    }
  }
  next();
}
 
const signUpVerify = {};
signUpVerify.checkDuplicateUserNameOrEmail = checkDuplicateUserNameOrEmail;
signUpVerify.checkRolesExisted = checkRolesExisted;
 
module.exports = signUpVerify;

router/verifyJwtToken.js

const jwt = require('jsonwebtoken');
const config = require('../config/config.js');
const db = require('../config/db.config.js');
const User = db.user;
 
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.findById(req.userId)
    .then(user => {
      user.getRoles().then(roles => {
        for(let i=0; i<roles.length; i++){
          console.log(roles[i].name);
          if(roles[i].name.toUpperCase() === "ADMIN"){
            next();
            return;
          }
        }
        
        res.status(403).send("Require Admin Role!");
        return;
      })
    })
}
 
isPmOrAdmin = (req, res, next) => {
  User.findById(req.userId)
    .then(user => {
      user.getRoles().then(roles => {
        for(let i=0; i<roles.length; i++){          
          if(roles[i].name.toUpperCase() === "PM"){
            next();
            return;
          }
          
          if(roles[i].name.toUpperCase() === "ADMIN"){
            next();
            return;
          }
        }
        
        res.status(403).send("Require PM or Admin Roles!");
      })
    })
}
 
const authJwt = {};
authJwt.verifyToken = verifyToken;
authJwt.isAdmin = isAdmin;
authJwt.isPmOrAdmin = isPmOrAdmin;
 
module.exports = authJwt;

Implement RestApis Controller

controller/controller.js

const db = require('../config/db.config.js');
const config = require('../config/config.js');
const User = db.user;
const Role = db.role;

const Op = db.Sequelize.Op;

var jwt = require('jsonwebtoken');
var bcrypt = require('bcryptjs');

exports.signup = (req, res) => {
	// Save User to Database
	User.create({
		name: req.body.name,
		username: req.body.username,
		email: req.body.email,
		password: bcrypt.hashSync(req.body.password, 8)
	}).then(user => {
		Role.findAll({
			where: {
				name: {
					[Op.or]: req.body.roles
				}
			}
		}).then(roles => {
			user.setRoles(roles).then(() => {
				res.send({ message: 'Registered successfully!' });
			});
		}).catch(err => {
			res.status(500).send({ reason: err.message });
		});
	}).catch(err => {
		res.status(500).send({ reason: err.message });
	})
}

exports.signin = (req, res) => {
	User.findOne({
		where: {
			username: req.body.username
		}
	}).then(user => {
		if (!user) {
			return res.status(404).send({ reason: 'User Not Found.' });
		}

		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
		});

		var authorities = [];
		user.getRoles().then(roles => {
			for (let i = 0; i < roles.length; i++) {
				authorities.push('ROLE_' + roles[i].name.toUpperCase());
			}
			res.status(200).send({
				auth: true,
				accessToken: token,
				username: user.username,
				authorities: authorities
			});
		})
	}).catch(err => {
		res.status(500).send({ reason: err.message });
	});
}

exports.userContent = (req, res) => {
	User.findOne({
		where: { id: req.userId },
		attributes: ['name', 'username', 'email'],
		include: [{
			model: Role,
			attributes: ['id', 'name'],
			through: {
				attributes: ['userId', 'roleId'],
			}
		}]
	}).then(user => {
		res.status(200).send({
			'description': '>>> User Contents!',
			'user': user
		});
	}).catch(err => {
		res.status(500).send({
			'description': 'Can not access User Page',
			'error': err
		});
	})
}

exports.adminBoard = (req, res) => {
	User.findOne({
		where: { id: req.userId },
		attributes: ['name', 'username', 'email'],
		include: [{
			model: Role,
			attributes: ['id', 'name'],
			through: {
				attributes: ['userId', 'roleId'],
			}
		}]
	}).then(user => {
		res.status(200).send({
			'description': '>>> Admin Contents',
			'user': user
		});
	}).catch(err => {
		res.status(500).send({
			'description': 'Can not access Admin Board',
			'error': err
		});
	})
}

exports.managementBoard = (req, res) => {
	User.findOne({
		where: { id: req.userId },
		attributes: ['name', 'username', 'email'],
		include: [{
			model: Role,
			attributes: ['id', 'name'],
			through: {
				attributes: ['userId', 'roleId'],
			}
		}]
	}).then(user => {
		res.status(200).send({
			'description': '>>> Project Management Board',
			'user': user
		});
	}).catch(err => {
		res.status(500).send({
			'description': 'Can not access Management Board',
			'error': err
		});
	})
}

We define jwt-secret-key & User Roles in config/config.js:

module.exports = {
  'secret': 'loizenai-super-secret-key',
  ROLEs: ['USER', 'ADMIN', 'PM']
};

Server

server.js

var express = require('express');
var app = express();
var bodyParser = require('body-parser');
app.use(bodyParser.json())

require('./app/router/router.js')(app);

const db = require('./app/config/db.config.js');

const Role = db.role;

// force: true will drop the table if it already exists
db.sequelize.sync({force: true}).then(() => {
  console.log('Drop and Resync with { force: true }');
  initial();
});

// 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.create({
		id: 1,
		name: "USER"
	});

	Role.create({
		id: 2,
		name: "PM"
	});

	Role.create({
		id: 3,
		name: "ADMIN"
	});
}

Build Frontent Angular JWT Token Based Authentication

How to build JWT Authentication frontend with Angular

Send Requests to Server

Add Token to Header with HttpInterceptor

We use Angular HttpInterceptor with intercept() method to inspect and transform HTTP requests (before they are sent to server):

import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
 
const TOKEN_HEADER_KEY = 'Authorization';
 
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
    ...
    intercept(req: HttpRequest, next: HttpHandler) {
        let authReq = req;
        const token = ...;
        if (token != null) {
            authReq = req.clone({ headers: req.headers.set(TOKEN_HEADER_KEY, 'Bearer ' + token) });
        }
        return next.handle(authReq);
    }
}
 
export const httpInterceptorProviders = [
    { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
];

– The HTTPRequest object will be inspected and forwarded to handle() method of the HttpHandler object.
handle() method transforms HTTPRequest object into an Observable of HttpEvents which includes the server’s response.

What is next: HttpHandler object?
-> This object represents the next interceptor in the chain of interceptors. The final ‘next’ in the chain is the Angular HttpClient handler.

HTTP Request using HttpClient

We send HTTP Requests (signin/signup) using Angular HttpClient:

import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
 
const httpOptions = {
  headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};
 
export class AuthService {
 
  private loginUrl = 'http://localhost:8080/api/auth/signin';
  private signupUrl = 'http://localhost:8080/api/auth/signup';
 
  constructor(private http: HttpClient) {
  }
 
  // JwtResponse(accessToken,type,username,authorities)
  attemptAuth(credentials: AuthLoginInfo): Observable {
    return this.http.post(this.loginUrl, credentials, httpOptions);
  }
 
  // SignUpInfo(name,username,email,role,password)
  signUp(info: SignUpInfo): Observable {
    return this.http.post(this.signupUrl, info, httpOptions);
  }
}

AuthLoginInfo fields & SignUpInfo fields are validated using Angular template-driven Form.

Once Token is saved, we can access protected resources simply:

export class UserService {
  private userUrl = 'http://localhost:8080/api/test/user';
  private pmUrl = 'http://localhost:8080/api/test/pm';
  private adminUrl = 'http://localhost:8080/api/test/admin';
 
  constructor(private http: HttpClient) { }
 
  getUserBoard(): Observable {
    return this.http.get(this.userUrl, { responseType: 'text' });
  }
 
  getPMBoard(): Observable {
    return this.http.get(this.pmUrl, { responseType: 'text' });
  }
 
  getAdminBoard(): Observable {
    return this.http.get(this.adminUrl, { responseType: 'text' });
  }
}

Handle Responses

Using AuthService to work with Observable object:

Signup Response
export class RegisterComponent implements OnInit {
  ...
  constructor(private authService: AuthService) { }
 
  onSubmit() {
    ...
    this.authService.signUp(this.signupInfo).subscribe(
      data => {
        ...
      },
      error => {
        ...
      }
    );
  }
}
Login Response

In addition to using AuthService to work with Observable object, we also call TokenStorageService methods to save Token, >Username, Authorities:

export class LoginComponent implements OnInit {
  ...
  constructor(private authService: AuthService, private tokenStorage: TokenStorageService) { }
 
  ngOnInit() {
    if (this.tokenStorage.getToken()) {
      this.isLoggedIn = true;
      this.roles = this.tokenStorage.getAuthorities();
    }
  }
 
  onSubmit() {
    ...
    this.authService.attemptAuth(this.loginInfo).subscribe(
      data => {
        this.tokenStorage.saveToken(data.accessToken);
        this.tokenStorage.saveUsername(data.username);
        this.tokenStorage.saveAuthorities(data.authorities);
 
        this.roles = this.tokenStorage.getAuthorities();
        ...
        reloadPage();
      },
      error => {
        ...
      }
    );
  }
}

Manage Token & User Logout

We use TokenStorageService to manage Token inside Browser’s sessionStorage:

export class TokenStorageService {
  private roles: Array<string> = [];
 
  public saveToken(token: string) {
    window.sessionStorage.removeItem(TOKEN_KEY);
    window.sessionStorage.setItem(TOKEN_KEY, token);
  }
 
  public getToken(): string {
    return sessionStorage.getItem(TOKEN_KEY);
  }
 
  public saveUsername(username: string) {
    window.sessionStorage.removeItem(USERNAME_KEY);
    window.sessionStorage.setItem(USERNAME_KEY, username);
  }
 
  public getUsername(): string {
    return sessionStorage.getItem(USERNAME_KEY);
  }
 
  public saveAuthorities(authorities: string[]) {
    window.sessionStorage.removeItem(AUTHORITIES_KEY);
    window.sessionStorage.setItem(AUTHORITIES_KEY, JSON.stringify(authorities));
  }
 
  public getAuthorities(): string[] {
    this.roles = [];
 
    if (sessionStorage.getItem(TOKEN_KEY)) {
      JSON.parse(sessionStorage.getItem(AUTHORITIES_KEY)).forEach(authority => {
        this.roles.push(authority.authority);
      });
    }
 
    return this.roles;
  }
}

For Logout action, we only need to clear Browser’s sessionStorage:

export class TokenStorageService {
  ...
  signOut() {
    window.sessionStorage.clear();
  }
}

Practice

Angular Client for JWT Authentication Overview

Goal

We will build Angular Client which allows users to register, login account. And depending on the role of current User (user, pm or admin), this system accepts what he can access:

Angular-Nodejs-Security-Jwt-Authentication-Work-Process-Diagram
Angular-Nodejs-Security-Jwt-Authentication-Work-Process-Diagram
Elements Overview
Angular Nodejs Jwt Authentication Architecture Diagram Front End Client
Angular Nodejs Jwt Authentication Architecture Diagram Front End Client

Look back to the session ANGULAR FRONT-END WITH INTERCEPTOR for more details.

Technologies

– Angular 6
– RxJS 6

Project Structure
Angular Project Structure
Angular Project Structure

Create Services & Components

Run commands below:
ng g s auth/auth
ng g s auth/token-storage
ng g s services/user

ng g c login
ng g c register
ng g c home
ng g c user
ng g c pm
ng g c admin

AppModule

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { HttpClientModule } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
 
import { AppComponent } from './app.component';
import { LoginComponent } from './login/login.component';
import { UserComponent } from './user/user.component';
import { RegisterComponent } from './register/register.component';
import { HomeComponent } from './home/home.component';
import { AdminComponent } from './admin/admin.component';
import { PmComponent } from './pm/pm.component';
 
import { httpInterceptorProviders } from './auth/auth-interceptor';
 
@NgModule({
  declarations: [
    AppComponent,
    LoginComponent,
    UserComponent,
    RegisterComponent,
    HomeComponent,
    AdminComponent,
    PmComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    FormsModule,
    HttpClientModule
  ],
  providers: [httpInterceptorProviders],
  bootstrap: [AppComponent]
})
export class AppModule { }

Services

Auth Models

auth/login-info.ts

export class AuthLoginInfo {
    username: string;
    password: string;
 
    constructor(username: string, password: string) {
        this.username = username;
        this.password = password;
    }
}

auth/sigup-info.ts

export class SignUpInfo {
    name: string;
    username: string;
    email: string;
    role: string[];
    password: string;
 
    constructor(name: string, username: string, email: string, password: string) {
        this.name = name;
        this.username = username;
        this.email = email;
        this.password = password;
        this.role = ['user'];
    }
}

auth/jwt-response.ts

export class JwtResponse {
    accessToken: string;
    type: string;
    username: string;
    authorities: string[];
}
Auth Service

auth/auth.service.ts

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
 
import { JwtResponse } from './jwt-response';
import { AuthLoginInfo } from './login-info';
import { SignUpInfo } from './signup-info';
 
const httpOptions = {
  headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};
 
@Injectable({
  providedIn: 'root'
})
export class AuthService {
 
  private loginUrl = 'http://localhost:8080/api/auth/signin';
  private signupUrl = 'http://localhost:8080/api/auth/signup';
 
  constructor(private http: HttpClient) {
  }
 
  attemptAuth(credentials: AuthLoginInfo): Observable {
    return this.http.post(this.loginUrl, credentials, httpOptions);
  }
 
  signUp(info: SignUpInfo): Observable {
    return this.http.post(this.signupUrl, info, httpOptions);
  }
}
Token Storage Service

auth/token-storage.service.ts

import { Injectable } from '@angular/core';
 
const TOKEN_KEY = 'AuthToken';
const USERNAME_KEY = 'AuthUsername';
const AUTHORITIES_KEY = 'AuthAuthorities';
 
@Injectable({
  providedIn: 'root'
})
export class TokenStorageService {
  private roles: Array = [];
  constructor() { }
 
  signOut() {
    window.sessionStorage.clear();
  }
 
  public saveToken(token: string) {
    window.sessionStorage.removeItem(TOKEN_KEY);
    window.sessionStorage.setItem(TOKEN_KEY, token);
  }
 
  public getToken(): string {
    return sessionStorage.getItem(TOKEN_KEY);
  }
 
  public saveUsername(username: string) {
    window.sessionStorage.removeItem(USERNAME_KEY);
    window.sessionStorage.setItem(USERNAME_KEY, username);
  }
 
  public getUsername(): string {
    return sessionStorage.getItem(USERNAME_KEY);
  }
 
  public saveAuthorities(authorities: string[]) {
    window.sessionStorage.removeItem(AUTHORITIES_KEY);
    window.sessionStorage.setItem(AUTHORITIES_KEY, JSON.stringify(authorities));
  }
 
  public getAuthorities(): string[] {
    this.roles = [];
 
    if (sessionStorage.getItem(TOKEN_KEY)) {
      JSON.parse(sessionStorage.getItem(AUTHORITIES_KEY)).forEach(authority => {
        this.roles.push(authority.authority);
      });
    }
 
    return this.roles;
  }
}
User Service

services/user.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
 
@Injectable({
  providedIn: 'root'
})
export class UserService {
 
  private userUrl = 'http://localhost:8080/api/test/user';
  private pmUrl = 'http://localhost:8080/api/test/pm';
  private adminUrl = 'http://localhost:8080/api/test/admin';
 
  constructor(private http: HttpClient) { }
 
  getUserBoard(): Observable {
    return this.http.get(this.userUrl, { responseType: 'text' });
  }
 
  getPMBoard(): Observable {
    return this.http.get(this.pmUrl, { responseType: 'text' });
  }
 
  getAdminBoard(): Observable {
    return this.http.get(this.adminUrl, { responseType: 'text' });
  }
}

HTTP Interceptor

auth/auth-interceptor.ts

import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
 
import { TokenStorageService } from './token-storage.service';
 
const TOKEN_HEADER_KEY = 'Authorization';
 
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
 
    constructor(private token: TokenStorageService) { }
 
    intercept(req: HttpRequest, next: HttpHandler) {
        let authReq = req;
        const token = this.token.getToken();
        if (token != null) {
            authReq = req.clone({ headers: req.headers.set(TOKEN_HEADER_KEY, 'Bearer ' + token) });
        }
        return next.handle(authReq);
    }
}
 
export const httpInterceptorProviders = [
    { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
];

Components

Home Component

home.component.ts

import { Component, OnInit } from '@angular/core';
 
import { TokenStorageService } from '../auth/token-storage.service';
 
@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
  info: any;
 
  constructor(private token: TokenStorageService) { }
 
  ngOnInit() {
    this.info = {
      token: this.token.getToken(),
      username: this.token.getUsername(),
      authorities: this.token.getAuthorities()
    };
  }
 
  logout() {
    this.token.signOut();
    window.location.reload();
  }
}

home.component.html

<div *ngIf="info.token; else loggedOut">
  <h5 class="text-primary">Your Information</h5>
  <p>
    <strong>Username:</strong> {{info.username}}<br/>
    <strong>Roles:</strong> {{info.authorities}}<br />
    <strong>Token:</strong> {{info.token}}.
  </p>
  <button class="btn btn-secondary" (click)="logout()">Logout</button>
</div>
 
<ng-template #loggedOut>
  Please login.
</ng-template>
Login Component

login.component.ts

import { Component, OnInit } from '@angular/core';
 
import { AuthService } from '../auth/auth.service';
import { TokenStorageService } from '../auth/token-storage.service';
import { AuthLoginInfo } from '../auth/login-info';
 
@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
  form: any = {};
  isLoggedIn = false;
  isLoginFailed = false;
  errorMessage = '';
  roles: string[] = [];
  private loginInfo: AuthLoginInfo;
 
  constructor(private authService: AuthService, private tokenStorage: TokenStorageService) { }
 
  ngOnInit() {
    if (this.tokenStorage.getToken()) {
      this.isLoggedIn = true;
      this.roles = this.tokenStorage.getAuthorities();
    }
  }
 
  onSubmit() {
    console.log(this.form);
 
    this.loginInfo = new AuthLoginInfo(
      this.form.username,
      this.form.password);
 
    this.authService.attemptAuth(this.loginInfo).subscribe(
      data => {
        this.tokenStorage.saveToken(data.accessToken);
        this.tokenStorage.saveUsername(data.username);
        this.tokenStorage.saveAuthorities(data.authorities);
 
        this.isLoginFailed = false;
        this.isLoggedIn = true;
        this.roles = this.tokenStorage.getAuthorities();
        this.reloadPage();
      },
      error => {
        console.log(error);
        this.errorMessage = error.error.message;
        this.isLoginFailed = true;
      }
    );
  }
 
  reloadPage() {
    window.location.reload();
  }
}

login.component.html

<div *ngIf="isLoggedIn; else loggedOut">
  Logged in as {{roles}}.
</div>
 
<ng-template #loggedOut>
  <div class="row col-sm-6" style="max-width:350px;">
    <form name="form" (ngSubmit)="f.form.valid && onSubmit()" #f="ngForm" novalidate>
      <div class="form-group">
        <label for="username">Username</label>
        <input type="text" class="form-control" name="username" [(ngModel)]="form.username" #username="ngModel"
          required />
        <div *ngIf="f.submitted && username.invalid">
          <div *ngIf="username.errors.required">Username is required</div>
        </div>
      </div>
      <div class="form-group">
        <label for="password">Password</label>
        <input type="password" class="form-control" name="password" [(ngModel)]="form.password" #password="ngModel"
          required minlength="6" />
        <div *ngIf="f.submitted && password.invalid">
          <div *ngIf="password.errors.required">Password is required</div>
          <div *ngIf="password.errors.minlength">Password must be at least 6 characters</div>
        </div>
      </div>
      <div class="form-group">
        <button class="btn btn-primary">Login</button>
        <div *ngIf="f.submitted && isLoginFailed" class="alert alert-danger">
          Login failed: {{errorMessage}}
        </div>
      </div>
    </form>
    <hr />
    <p>Don't have an account?</p>
    <a href="signup" class="btn btn-success">Sign Up</a>
  </div>
</ng-template>
Register Component

register.component.ts

import { Component, OnInit } from '@angular/core';
 
import { AuthService } from '../auth/auth.service';
import { SignUpInfo } from '../auth/signup-info';
 
@Component({
  selector: 'app-register',
  templateUrl: './register.component.html',
  styleUrls: ['./register.component.css']
})
export class RegisterComponent implements OnInit {
  form: any = {};
  signupInfo: SignUpInfo;
  isSignedUp = false;
  isSignUpFailed = false;
  errorMessage = '';
 
  constructor(private authService: AuthService) { }
 
  ngOnInit() { }
 
  onSubmit() {
    console.log(this.form);
 
    this.signupInfo = new SignUpInfo(
      this.form.name,
      this.form.username,
      this.form.email,
      this.form.password);
 
    this.authService.signUp(this.signupInfo).subscribe(
      data => {
        console.log(data);
        this.isSignedUp = true;
        this.isSignUpFailed = false;
      },
      error => {
        console.log(error);
        this.errorMessage = error.error.message;
        this.isSignUpFailed = true;
      }
    );
  }
}

register.component.html

<div *ngIf="isSignedUp; else signupForm">
  Your registration is successful. Please login!
</div>
 
<ng-template #signupForm>
  <div class="row col-sm-6" style="max-width:350px;">
    <form name="form" (ngSubmit)="f.form.valid && onSubmit()" #f="ngForm" novalidate>
      <div class="form-group">
        <label for="name">Your name</label>
        <input type="text" class="form-control" name="name" [(ngModel)]="form.name" #name="ngModel" required />
        <div *ngIf="f.submitted && name.invalid">
          <div *ngIf="name.errors.required">Name is required</div>
        </div>
      </div>
      <div class="form-group">
        <label for="username">Username</label>
        <input type="text" class="form-control" name="username" [(ngModel)]="form.username" #username="ngModel"
          required />
        <div *ngIf="f.submitted && username.invalid">
          <div *ngIf="username.errors.required">Username is required</div>
        </div>
      </div>
      <div class="form-group">
        <label for="email">Email</label>
        <input type="text" class="form-control" name="email" [(ngModel)]="form.email" #email="ngModel" required email />
        <div *ngIf="f.submitted && email.invalid">
          <div *ngIf="email.errors.required">Email is required</div>
          <div *ngIf="email.errors.email">Email must be a valid email address</div>
        </div>
      </div>
      <div class="form-group">
        <label for="password">Password</label>
        <input type="password" class="form-control" name="password" [(ngModel)]="form.password" #password="ngModel"
          required minlength="6" />
        <div *ngIf="f.submitted && password.invalid">
          <div *ngIf="password.errors.required">Password is required</div>
          <div *ngIf="password.errors.minlength">Password must be at least 6 characters</div>
        </div>
      </div>
      <div class="form-group">
        <button class="btn btn-primary">Register</button>
        <div *ngIf="f.submitted && isSignUpFailed" class="alert alert-warning">
          Signup failed!<br/>{{errorMessage}}
        </div>
      </div>
    </form>
  </div>
</ng-template>
[UserRole] Components

user/pm/admin.component.html

<h4>Content from Server</h4>
{{board}}
{{errorMessage}}

user.component.ts

import { Component, OnInit } from '@angular/core';
import { UserService } from '../services/user.service';
 
@Component({
  selector: 'app-user',
  templateUrl: './user.component.html',
  styleUrls: ['./user.component.css']
})
export class UserComponent implements OnInit {
  board: string;
  errorMessage: string;
 
  constructor(private userService: UserService) { }
 
  ngOnInit() {
    this.userService.getUserBoard().subscribe(
      data => {
        this.board = data;
      },
      error => {
        this.errorMessage = `${error.status}: ${JSON.parse(error.error).message}`;
      }
    );
  }
}

pm.component.ts

import { Component, OnInit } from '@angular/core';
import { UserService } from '../services/user.service';
 
@Component({
  selector: 'app-pm',
  templateUrl: './pm.component.html',
  styleUrls: ['./pm.component.css']
})
export class PmComponent implements OnInit {
  board: string;
  errorMessage: string;
 
  constructor(private userService: UserService) { }
 
  ngOnInit() {
    this.userService.getPMBoard().subscribe(
      data => {
        this.board = data;
      },
      error => {
        this.errorMessage = `${error.status}: ${JSON.parse(error.error).message}`;
      }
    );
  }
}

admin.component.ts

import { Component, OnInit } from '@angular/core';
import { UserService } from '../services/user.service';
 
@Component({
  selector: 'app-admin',
  templateUrl: './admin.component.html',
  styleUrls: ['./admin.component.css']
})
export class AdminComponent implements OnInit {
  board: string;
  errorMessage: string;
 
  constructor(private userService: UserService) { }
 
  ngOnInit() {
    this.userService.getAdminBoard().subscribe(
      data => {
        this.board = data;
      },
      error => {
        this.errorMessage = `${error.status}: ${JSON.parse(error.error).message}`;
      }
    );
  }
}

App Routing

app-routing.module.ts

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
 
import { RegisterComponent } from './register/register.component';
import { LoginComponent } from './login/login.component';
import { HomeComponent } from './home/home.component';
import { UserComponent } from './user/user.component';
import { PmComponent } from './pm/pm.component';
import { AdminComponent } from './admin/admin.component';
 
const routes: Routes = [
    {
        path: 'home',
        component: HomeComponent
    },
    {
        path: 'user',
        component: UserComponent
    },
    {
        path: 'pm',
        component: PmComponent
    },
    {
        path: 'admin',
        component: AdminComponent
    },
    {
        path: 'auth/login',
        component: LoginComponent
    },
    {
        path: 'signup',
        component: RegisterComponent
    },
    {
        path: '',
        redirectTo: 'home',
        pathMatch: 'full'
    }
];
 
@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule]
})
export class AppRoutingModule { }

app.component.ts

import { Component, OnInit } from '@angular/core';
import { TokenStorageService } from './auth/token-storage.service';
 
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  private roles: string[];
  private authority: string;
 
  constructor(private tokenStorage: TokenStorageService) { }
 
  ngOnInit() {
    if (this.tokenStorage.getToken()) {
      this.roles = this.tokenStorage.getAuthorities();
      this.roles.every(role => {
        if (role === 'ROLE_ADMIN') {
          this.authority = 'admin';
          return false;
        } else if (role === 'ROLE_PM') {
          this.authority = 'pm';
          return false;
        }
        this.authority = 'user';
        return true;
      });
    }
  }
}

app.component.html

<nav class="navbar navbar-inverse">
  <div class="container-fluid">
    <div class="navbar-header">
      <a class="navbar-brand" href="#">grokonez</a>
    </div>
    <ul class="nav navbar-nav" routerLinkActive="active">
      <li class="nav-item"><a class="nav-link" routerLink="home">Home</a></li>
      <li *ngIf="authority === 'user'" class="nav-item">
        <a class="nav-link" routerLink="user">User Board</a>
      </li>
      <li *ngIf="authority === 'admin'" class="nav-item">
        <a class="nav-link" routerLink="admin">Admin Board</a>
      </li>
      <li *ngIf="authority === 'pm'" class="nav-item">
        <a class="nav-link" routerLink="pm">PM Board</a>
      </li>
      <li *ngIf="!authority" class="nav-item">
        <a class="nav-link" routerLink="auth/login">Login</a>
      </li>
    </ul>
  </div>
</nav>
<div class="container">
  <router-outlet></router-outlet>
</div>

Sourcecodes

I includes 2 sourcecodes for the tutorial: “Angular 10 + Nodejs JWT Token Based Authentication Example”

  • Nodejs Backend
  • Angular Frontend

– All Features of sourcecode for Nodejs Backend:

  • Create Sequelize Models
    • User
    • Role
  • Sequelize Database Configuration
  • Define RestAPIs Router with Middleware
    • RestAPIs Router
    • Middleware functions
  • Implement RestApis Controller

Nodejs JWT Token Based Authentication Example

* GitHUB Sourcecode for Nodejs JWT Token Based Authentication Example:

Nodejs JWT Token Based Authentication Examples – GitHub Sourcecode

– All Features of sourcecode for Angular Frontend:

  • Build Angular Services:
    • Auth Service
    • Token Storage Service
    • User Service
  • HTTP Interceptor
  • Build Angular Components:
    • Home Component
    • Login Component
    • Register Component
    • [UserRole] Components
  • App Routing

Angular-10-JWT-Token-Based-Authentication-Example

* Github Sourcecode – Angular JWT Token Based Authentication Example:

Angular JWT Authentication Example

Thanks for reading!

Further Reading

Leave a Reply

Your email address will not be published. Required fields are marked *