# Role Based JWT Authentication
Here you will find how Role Based JWT authentication works, which is provided by our template.
TIP
To implement JWT authentication in the starter-kit read article from here.
To implement Role Based authentication in the starter-kit read article from here.
# Folder structure
├── app
│ ├── auth -> Auth folder contains all files
│ │ ├── helpers -> Contains all helper files
│ │ │ ├── auth.guards.ts -> Auth guard file
│ │ │ ├── error.interceptor.ts -> Error inspector file
│ │ │ ├── fake-backend.ts -> Fake backend file
│ │ │ ├── jwt.interceptor.ts -> Jwt inspector file
│ │ │ └── index.ts -> Index file, helper files are imported
│ │ ├── service -> Service Folder
│ │ │ ├── authentication.service.ts -> Auth service file
│ │ │ ├── user.service.ts -> User service file to manage user
│ │ │ └── index.ts -> Index file, where all service files are imported
│ │ ├── models -> Model Folder for role & user management
│ │ │ ├── role.ts -> Role file to manage role
│ │ │ ├── user.ts -> User file (model for user properties)
│ │ │ └── index.ts -> Index file, where all model files are imported
# Auth
You can find our Role-Based JWT Auth files in auth
folder, Path: starter-kit/src/app/auth
.
Let's understand what each file has to offer:
# jwt.interceptor.ts
The JWT Interceptor intercepts HTTP requests from the application to add a JWT auth token to the Authorization header if the user is logged in and the request to the application API URL(environment.apiUrl)
.
It's implemented using the HttpInterceptor
class included in the HttpClientModule
, by extending the HttpInterceptor
class you can create a custom interceptor to modify HTTP requests before they get sent to the server.
TIP
For more details about HttpInterceptor
click here (opens new window).
NOTE
HTTP_INTERCEPTORS
are added to the request pipeline in the providers
array of the app.module.ts
file.
import { Injectable } from '@angular/core'
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'
import { Observable } from 'rxjs'
import { environment } from 'environments/environment'
import { AuthenticationService } from 'app/auth/service'
@Injectable()
export class JwtInterceptor implements HttpInterceptor {
/**
*
* @param {AuthenticationService} _authenticationService
*/
constructor(private _authenticationService: AuthenticationService) {}
/**
* Add auth header with jwt if user is logged in and request is to api url
* @param request
* @param next
*/
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const currentUser = this._authenticationService.currentUserValue
const isLoggedIn = currentUser && currentUser.token
const isApiUrl = request.url.startsWith(environment.apiUrl)
if (isLoggedIn && isApiUrl) {
request = request.clone({
setHeaders: {
Authorization: `Bearer ${currentUser.token}`
}
})
}
return next.handle(request)
}
}
# fake-backend.ts
TIP
You can modify fake users in this file.
WARNING
As we are using fake backend we are not generating a real JWT token.
A fake backend that intercepts the HTTP requests from the Angular app and sends back "fake" responses. This is done by a class that implements the Angular HttpInterceptor
interface, for more information on Angular HTTP Interceptors (opens new window)
The fake backend contains a handleRoute()
function that checks if the request matches one of the faked routes in the switch statement, at the moment this includes POST requests to the /users/authenticate
route for handling authentication, and GET requests to the /users
route for getting all users.
Requests to the authenticate route are handled by the authenticate()
function which checks the email
and password
against an array of hardcoded users. If the email
and password
are correct then an ok
response is returned with the user details and a fake jwt token, otherwise, an error response is returned.
Requests to the get users route are handled by the getUsers()
function which checks if the user is logged in by calling the new isLoggedIn()
helper function. If the user is logged in an ok()
response with the whole users array is returned, otherwise, a 401 Unauthorized response is returned by calling the new unauthorized()
helper function.
If the request doesn't match any of the faked routes it is passed through as a real HTTP request to the backend API.
/**
* ? Tip:
*
* For Actual Node.js - Role Based Authorization Tutorial with Example API
* Refer: https://jasonwatmore.com/post/2018/11/28/nodejs-role-based-authorization-tutorial-with-example-api
* Running an Angular 9 client app with the Node.js Role Based Auth API
*/
import { Injectable } from '@angular/core'
import {
HttpRequest,
HttpResponse,
HttpHandler,
HttpEvent,
HttpInterceptor,
HTTP_INTERCEPTORS
} from '@angular/common/http'
import { Observable, of, throwError } from 'rxjs'
import { delay, mergeMap, materialize, dematerialize } from 'rxjs/operators'
import { User, Role } from 'app/auth/models'
// Users with role
const users: User[] = [
{
id: 1,
email: 'admin@demo.com',
password: 'admin',
firstName: 'John',
lastName: 'Doe',
avatar: 'avatar-s-11.jpg',
role: Role.Admin
},
{
id: 2,
email: 'client@demo.com',
password: 'client',
firstName: 'Nataly',
lastName: 'Doe',
avatar: 'avatar-s-2.jpg',
role: Role.Client
},
{
id: 3,
email: 'user@demo.com',
password: 'user',
firstName: 'Rose',
lastName: 'Doe',
avatar: 'avatar-s-3.jpg',
role: Role.User
}
]
@Injectable()
export class FakeBackendInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const { url, method, headers, body } = request
// wrap in delayed observable to simulate server api call
return of(null).pipe(mergeMap(handleRoute))
// .pipe(materialize()) // call materialize and dematerialize to ensure delay even if an error is thrown (https://github.com/Reactive-Extensions/RxJS/issues/648)
// .pipe(delay(500))
// .pipe(dematerialize());
function handleRoute() {
switch (true) {
case url.endsWith('/users/authenticate') && method === 'POST':
return authenticate()
case url.endsWith('/users') && method === 'GET':
return getUsers()
case url.match(/\/users\/\d+$/) && method === 'GET':
return getUserById()
default:
// pass through any requests not handled above
return next.handle(request)
}
}
// route functions
function authenticate() {
const { email, password } = body
const user = users.find(x => x.email === email && x.password === password)
if (!user) return error('Username or password is incorrect')
return ok({
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
avatar: user.avatar,
role: user.role,
token: `fake-jwt-token.${user.id}`
})
}
function getUsers() {
if (!isAdmin()) return unauthorized()
return ok(users)
}
function getUserById() {
if (!isLoggedIn()) return unauthorized()
// only admins can access other user records
if (!isAdmin() && currentUser().id !== idFromUrl()) return unauthorized()
const user = users.find(x => x.id === idFromUrl())
return ok(user)
}
// helper functions
function ok(body) {
return of(new HttpResponse({ status: 200, body }))
}
function unauthorized() {
return throwError({ status: 401, error: { message: 'unauthorized' } })
}
function error(message) {
return throwError({ status: 400, error: { message } })
}
function isLoggedIn() {
const authHeader = headers.get('Authorization') || ''
return authHeader.startsWith('Bearer fake-jwt-token')
}
function isAdmin() {
return isLoggedIn() && currentUser().role === Role.Admin
}
function currentUser() {
if (!isLoggedIn()) return
const id = parseInt(headers.get('Authorization').split('.')[1])
return users.find(x => x.id === id)
}
function idFromUrl() {
const urlParts = url.split('/')
return parseInt(urlParts[urlParts.length - 1])
}
}
}
export const fakeBackendProvider = {
// use fake backend in place of Http service for backend-less development
provide: HTTP_INTERCEPTORS,
useClass: FakeBackendInterceptor,
multi: true
}
# error.interceptor.ts
The Error Interceptor intercepts HTTP responses from the API to check if there were any errors.
NOTE
If there is a 401 or 403 Unauthorized response the user will be redirected to the not-authorized page.
For all other errors, it will appear in the console of the browser.
It's implemented using the HttpInterceptor
class included in the HttpClientModule
, by extending the HttpInterceptor class you can create a custom interceptor to catch all error responses from the server in a single location.
import { Injectable } from '@angular/core'
import { Router } from '@angular/router'
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'
import { Observable, throwError } from 'rxjs'
import { catchError } from 'rxjs/operators'
import { AuthenticationService } from 'app/auth/service'
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
/**
* @param {Router} _router
* @param {AuthenticationService} _authenticationService
*/
constructor(private _router: Router, private _authenticationService: AuthenticationService) {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request).pipe(
catchError(err => {
if ([401, 403].indexOf(err.status) !== -1) {
// auto logout if 401 Unauthorized or 403 Forbidden response returned from api
this._router.navigate(['/pages/miscellaneous/not-authorized'])
// ? Can also logout and reload if needed
// this._authenticationService.logout();
// location.reload(true);
}
// throwError
const error = err.error.message || err.statusText
return throwError(error)
})
)
}
}
# auth.guards.ts
The auth guard is an angular route guard that's used to prevent unauthenticated users from accessing restricted routes, it does this by implementing the CanActivate
interface which allows the guard to decide if a route can be activated with the canActivate()
method. If the method returns true the route is activated (allowed to proceed), otherwise if the method returns false the route is blocked.
TIP
For more information about CanActivate
click here (opens new window)
The auth guard uses the authentication service to check if the user is logged in, if they are logged in it returns true
from the canActivate()
method, otherwise, it returns false
and redirects the user to the login page.
import { Injectable } from '@angular/core'
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'
import { AuthenticationService } from 'app/auth/service'
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
/**
*
* @param {Router} _router
* @param {AuthenticationService} _authenticationService
*/
constructor(private _router: Router, private _authenticationService: AuthenticationService) {}
// canActivate
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const currentUser = this._authenticationService.currentUserValue
if (currentUser) {
// check if route is restricted by role
if (route.data.roles && route.data.roles.indexOf(currentUser.role) === -1) {
// role not authorised so redirect to not-authorized page
this._router.navigate(['/pages/miscellaneous/not-authorized'])
return false
}
// authorised so return true
return true
}
// not logged in so redirect to login page with the return url
this._router.navigate(['/pages/authentication/login-v2'], { queryParams: { returnUrl: state.url } })
return false
}
}
# Service
# Authentication Service
Path: starter-kit/src/app/auth/service/authentication.service.ts
The authentication service is used to login & logout of the Angular app, it notifies other components when the user logs in & out and allows access to the currently logged in user.
RxJS Subjects and Observables are used to store the current user object and notify other components when the user logs in and out of the app. Angular components can subscribe()
to the public currentUser:
Observable property to be notified of changes, and notifications are sent when the this.currentUserSubject.next()
method is called in the login()
and logout()
methods, passing the argument to each subscriber. The RxJS BehaviorSubject
is a special type of Subject that keeps hold of the current value and emits it to any new subscribers as soon as they subscribe, while regular Subjects don't store the current value and only emit values that are published after a subscription is created.
The login()
method sends the user credentials
to the API via an HTTP POST request for authentication. If successful the user object including a JWT auth token are stored in localStorage to keep the user logged in between page refreshes. The user object is then published to all subscribers with the call to this.currentUserSubject.next(user);
.
The constructor()
of the service initializes the currentUserSubject
with the currentUser
object from localStorage
which enables the user to stay logged in between page refreshes or after the browser is closed. The public currentUser
property is then set to this.currentUserSubject.asObservable();
which allows other components to subscribe to the currentUser
Observable but doesn't allow them to publish to the currentUserSubject, this is so logging in and out of the app can only be done via the authentication service.
The currentUserValue
getter allows other components an easy way to get the value of the currently logged in user without having to subscribe to the currentUser
Observable.
The logout()
method removes the current user object from local storage and publishes null to the currentUserSubject
to notify all subscribers that the user has logged out.
import { Injectable } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { BehaviorSubject, Observable } from 'rxjs'
import { map } from 'rxjs/operators'
import { environment } from 'environments/environment'
import { User, Role } from 'app/auth/models'
import { ToastrService } from 'ngx-toastr'
@Injectable({ providedIn: 'root' })
export class AuthenticationService {
//public
public currentUser: Observable<User>
//private
private currentUserSubject: BehaviorSubject<User>
/**
*
* @param {HttpClient} _http
* @param {ToastrService} _toastrService
*/
constructor(private _http: HttpClient, private _toastrService: ToastrService) {
this.currentUserSubject = new BehaviorSubject<User>(JSON.parse(localStorage.getItem('currentUser')))
this.currentUser = this.currentUserSubject.asObservable()
}
// getter: currentUserValue
public get currentUserValue(): User {
return this.currentUserSubject.value
}
/**
* Confirms if user is admin
*/
get isAdmin() {
return this.currentUser && this.currentUserSubject.value.role === Role.Admin
}
/**
* Confirms if user is client
*/
get isClient() {
return this.currentUser && this.currentUserSubject.value.role === Role.Client
}
/**
* User login
*
* @param email
* @param password
* @returns user
*/
login(email: string, password: string) {
return this._http
.post<any>(`${environment.apiUrl}/users/authenticate`, { email, password })
.pipe(
map(user => {
// login successful if there's a jwt token in the response
if (user && user.token) {
// store user details and jwt token in local storage to keep user logged in between page refreshes
localStorage.setItem('currentUser', JSON.stringify(user))
// Display welcome toast!
setTimeout(() => {
this._toastrService.success(
'You have successfully logged in as an ' +
user.role +
' user to Vuexy. Now you can start to explore. Enjoy! 🎉',
'👋 Welcome, ' + user.firstName + '!',
{ toastClass: 'toast ngx-toastr', closeButton: true }
)
}, 2500)
// notify
this.currentUserSubject.next(user)
}
return user
})
)
}
/**
* User logout
*
*/
logout() {
// remove user from local storage to log user out
localStorage.removeItem('currentUser')
// notify
this.currentUserSubject.next(null)
}
}
# User Service
Path: starter-kit/src/app/auth/service/user.service.ts
The user service contains a method for getting all users from the API, I included it to demonstrate accessing a secure API endpoint with the http authorization header set after logging in to the application, the auth header is set with a JWT token with the JWT Interceptor above. The secure endpoint in the example is a fake one implemented in the fake backend provider above.
import { Injectable } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { environment } from 'environments/environment'
import { User } from 'app/auth/models'
@Injectable({ providedIn: 'root' })
export class UserService {
/**
*
* @param {HttpClient} _http
*/
constructor(private _http: HttpClient) {}
/**
* Get all users
*/
getAll() {
return this._http.get<User[]>(`${environment.apiUrl}/users`)
}
/**
* Get user by id
*/
getById(id: number) {
return this._http.get<User>(`${environment.apiUrl}/users/${id}`)
}
}
# Models
# role.ts
Path : starter-kit/src/app/auth/models/role.ts
The role model is a enum
that defines the all possible roles of a user.
TIP
You can add / remove roles as per preferences, Example : For a visitor role you can add Visitor='Visitor'
export enum Role {
Admin = 'Admin',
Client = 'Client',
User = 'User'
}
# user.ts
Path : starter-kit/src/app/auth/models/user.ts
The user model is a class
that defines the properties of a user.
import { Role } from './role'
export class User {
id: number
email: string
password: string
firstName: string
lastName: string
avatar: string
role: Role
token?: string
}