Tập tành dùng JWT và Angular Router and HttpInterceptor
16th Jul 2022Flow for User Registration and User Login
For JWT – Token based Authentication with Web API, we’re gonna call 2 endpoints:
- POST api/auth/signup for User Registration
- POST api/auth/signin for User Login
You can take a look at following flow to have an overview of Requests and Responses that Angular 13 JWT Auth Client will make or receive.
Angular Client must add a JWT to HTTP Authorization Header (or x-access-token Header) before sending request to protected resources. This can be done by using HttpInterceptor.
Component Diagram with Router and HttpInterceptor
Now look at the diagram below.
– The App component is a container using Router. It gets user token & user information from Browser Session Storage via token-storage.service. Then the navbar now can display based on the user login state & roles.
– Login & Register components have form for submission data (with support of Form Validation). They use token-storage.service for checking state and auth.service for sending signin/signup requests.
– auth.service uses Angular HttpClient ($http service) to make authentication requests.
– every HTTP request by $http service will be inspected and transformed before being sent by auth-interceptor.
– Home component is public for all visitor.
– Profile component get user data from Session Storage.
– BoardUser, BoardModerator, BoardAdmin components will be displayed depending on roles from Session Storage. In these components, we use user.service to get protected resources from API.
Technology
– Angular 13
– RxJS 7
– Angular CLI 13
Setup Angular 13 Jwt Authentication Project
Let’s open cmd and use Angular CLI to create a new Angular Project as following command:
ng new Angular13JwtAuth
- Would you like to add Angular routing? Yes
- Which stylesheet format would you like to use? CSS
We also need to generate some Components and Services:
ng g s _services/auth ng g s _services/token-storage ng g s _services/user ng g c login ng g c register ng g c home ng g c profile ng g c board-admin ng g c board-moderator ng g c board-user
After the previous process is done, under src folder, let’s create _helpers folder and auth.interceptor.ts file inside.
Now you can see that our project directory structure looks like this.
Project Structure
With the explanation in Component Diagram above, you can easily understand this project structure.
Setup App Module
Open app.module.ts, then import FormsModule & HttpClientModule.
We also need to add authInterceptorProviders in providers. I will show you how to define it later on this tutorial (in auth.interceptor.ts).
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { HttpClientModule } from '@angular/common/http'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { LoginComponent } from './login/login.component'; import { RegisterComponent } from './register/register.component'; import { HomeComponent } from './home/home.component'; import { ProfileComponent } from './profile/profile.component'; import { BoardAdminComponent } from './board-admin/board-admin.component'; import { BoardModeratorComponent } from './board-moderator/board-moderator.component'; import { BoardUserComponent } from './board-user/board-user.component'; import { authInterceptorProviders } from './_helpers/auth.interceptor'; @NgModule({ declarations: [ AppComponent, LoginComponent, RegisterComponent, HomeComponent, ProfileComponent, BoardAdminComponent, BoardModeratorComponent, BoardUserComponent ], imports: [ BrowserModule, AppRoutingModule, FormsModule, HttpClientModule ], providers: [authInterceptorProviders], bootstrap: [AppComponent] }) export class AppModule { }
Create Services
Authentication Service
This service sends signup, login HTTP POST requests to back-end.
_services/auth.service.ts
import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable } from 'rxjs'; const AUTH_API = 'http://localhost:8080/api/auth/'; const httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json' }) }; @Injectable({ providedIn: 'root' }) export class AuthService { constructor(private http: HttpClient) { } login(username: string, password: string): Observable<any> { return this.http.post(AUTH_API + 'signin', { username, password }, httpOptions); } register(username: string, email: string, password: string): Observable<any> { return this.http.post(AUTH_API + 'signup', { username, email, password }, httpOptions); } }
Token Storage Service
TokenStorageService to manages token and user information (username, email, roles) inside Browser’s Session Storage. For Logout, we only need to clear this Session Storage.
_services/token-storage.service.ts
import { Injectable } from '@angular/core'; const TOKEN_KEY = 'auth-token'; const USER_KEY = 'auth-user'; @Injectable({ providedIn: 'root' }) export class TokenStorageService { constructor() { } signOut(): void { window.sessionStorage.clear(); } public saveToken(token: string): void { window.sessionStorage.removeItem(TOKEN_KEY); window.sessionStorage.setItem(TOKEN_KEY, token); } public getToken(): string | null { return window.sessionStorage.getItem(TOKEN_KEY); } public saveUser(user: any): void { window.sessionStorage.removeItem(USER_KEY); window.sessionStorage.setItem(USER_KEY, JSON.stringify(user)); } public getUser(): any { const user = window.sessionStorage.getItem(USER_KEY); if (user) { return JSON.parse(user); } return {}; } }
Data Service
This service provides methods to access public and protected resources.
_services/user.service.ts
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; const API_URL = 'http://localhost:8080/api/test/'; @Injectable({ providedIn: 'root' }) export class UserService { constructor(private http: HttpClient) { } getPublicContent(): Observable<any> { return this.http.get(API_URL + 'all', { responseType: 'text' }); } getUserBoard(): Observable<any> { return this.http.get(API_URL + 'user', { responseType: 'text' }); } getModeratorBoard(): Observable<any> { return this.http.get(API_URL + 'mod', { responseType: 'text' }); } getAdminBoard(): Observable<any> { return this.http.get(API_URL + 'admin', { responseType: 'text' }); } }
You can see that it’s simple because we have HttpInterceptor.
Http Interceptor
HttpInterceptor has intercept() method to inspect and transform HTTP requests before they are sent to server.
AuthInterceptor implements HttpInterceptor. We’re gonna add Authorization header with ‘Bearer’ prefix to the token.
_helpers/auth.interceptor.ts
import { HTTP_INTERCEPTORS, HttpEvent } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http'; import { TokenStorageService } from '../_services/token-storage.service'; import { Observable } from 'rxjs'; const TOKEN_HEADER_KEY = 'Authorization'; // for Spring Boot back-end @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor(private token: TokenStorageService) { } intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { 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 authInterceptorProviders = [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true } ];
intercept() gets HTTPRequest object, change it and forward to HttpHandler object’s handle() method. It transforms HTTPRequest object into an Observable<HttpEvents>.
next: HttpHandler object represents the next interceptor in the chain of interceptors. The final ‘next’ in the chain is the Angular HttpClient.
Please use x-access-token header like this:
const TOKEN_HEADER_KEY = 'x-access-token'; @Injectable() export class AuthInterceptor implements HttpInterceptor { ... intercept(req: HttpRequest, next: HttpHandler): Observable<HttpEvent<any>> { ... if (token != null) { authReq = req.clone({ headers: req.headers.set(TOKEN_HEADER_KEY, token) }); } return next.handle(authReq); } }
Add Bootstrap to Angular project
Open index.html and import Bootstrap inside <head /> tag.
<!DOCTYPE html> <html lang="en"> <head> ... <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous" /> </head> <body> <app-root></app-root> </body> </html>
Create Components for Authentication
Register Component
This component binds form data (username, email, password) from template to AuthService.register() method that returns an Observable object.
register/register.component.ts
import { Component, OnInit } from '@angular/core'; import { AuthService } from '../_services/auth.service'; @Component({ selector: 'app-register', templateUrl: './register.component.html', styleUrls: ['./register.component.css'] }) export class RegisterComponent implements OnInit { form: any = { username: null, email: null, password: null }; isSuccessful = false; isSignUpFailed = false; errorMessage = ''; constructor(private authService: AuthService) { } ngOnInit(): void { } onSubmit(): void { const { username, email, password } = this.form; this.authService.register(username, email, password).subscribe({ next: data => { console.log(data); this.isSuccessful = true; this.isSignUpFailed = false; }, error: err => { this.errorMessage = err.error.message; this.isSignUpFailed = true; } }); } }
We use Form Validation in the template:
username: required, minLength=3, maxLength=20 email: required, email format password: required, minLength=6
register/register.component.html
<div class="col-md-12"> <div class="card card-container"> <img id="profile-img" src="//ssl.gstatic.com/accounts/ui/avatar_2x.png" class="profile-img-card" /> <form *ngIf="!isSuccessful" 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" required minlength="3" maxlength="20" #username="ngModel" /> <div class="alert-danger" *ngIf="username.errors && f.submitted"> <div *ngIf="username.errors['required']">Username is required</div> <div *ngIf="username.errors['minlength']"> Username must be at least 3 characters </div> <div *ngIf="username.errors['maxlength']"> Username must be at most 20 characters </div> </div> </div> <div class="form-group"> <label for="email">Email</label> <input type="email" class="form-control" name="email" [(ngModel)]="form.email" required email #email="ngModel" /> <div class="alert-danger" *ngIf="email.errors && f.submitted"> <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" required minlength="6" #password="ngModel" /> <div class="alert-danger" *ngIf="password.errors && f.submitted"> <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 btn-block">Sign Up</button> </div> <div class="alert alert-warning" *ngIf="f.submitted && isSignUpFailed"> Signup failed!<br />{{ errorMessage }} </div> </form> <div class="alert alert-success" *ngIf="isSuccessful"> Your registration is successful! </div> </div> </div>
In the code above, we use Template Driven Form
Login Component
Login Component also uses AuthService to work with Observable object. Besides that, it calls TokenStorageService methods to check loggedIn status and save Token, User info to Session Storage.
login/login.component.ts
import { Component, OnInit } from '@angular/core'; import { AuthService } from '../_services/auth.service'; import { TokenStorageService } from '../_services/token-storage.service'; @Component({ selector: 'app-login', templateUrl: './login.component.html', styleUrls: ['./login.component.css'] }) export class LoginComponent implements OnInit { form: any = { username: null, password: null }; isLoggedIn = false; isLoginFailed = false; errorMessage = ''; roles: string[] = []; constructor(private authService: AuthService, private tokenStorage: TokenStorageService) { } ngOnInit(): void { if (this.tokenStorage.getToken()) { this.isLoggedIn = true; this.roles = this.tokenStorage.getUser().roles; } } onSubmit(): void { const { username, password } = this.form; this.authService.login(username, password).subscribe({ next: data => { this.tokenStorage.saveToken(data.accessToken); this.tokenStorage.saveUser(data); this.isLoginFailed = false; this.isLoggedIn = true; this.roles = this.tokenStorage.getUser().roles; this.reloadPage(); }, error: err => { this.errorMessage = err.error.message; this.isLoginFailed = true; } }); } reloadPage(): void { window.location.reload(); } }
Here are what we validate in the form:
username: required password: required, minLength=6
login/login.component.html
<div class="col-md-12"> <div class="card card-container"> <img id="profile-img" src="//ssl.gstatic.com/accounts/ui/avatar_2x.png" class="profile-img-card" /> <form *ngIf="!isLoggedIn" 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" required #username="ngModel" /> <div class="alert alert-danger" role="alert" *ngIf="username.errors && f.submitted" > Username is required! </div> </div> <div class="form-group"> <label for="password">Password</label> <input type="password" class="form-control" name="password" [(ngModel)]="form.password" required minlength="6" #password="ngModel" /> <div class="alert alert-danger" role="alert" *ngIf="password.errors && f.submitted" > <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 btn-block"> Login </button> </div> <div class="form-group"> <div class="alert alert-danger" role="alert" *ngIf="f.submitted && isLoginFailed" > Login failed: {{ errorMessage }} </div> </div> </form> <div class="alert alert-success" *ngIf="isLoggedIn"> Logged in as {{ roles }}. </div> </div> </div>
Profile Component
This Component gets current User from Storage using TokenStorageService and show information (username, token, email, roles).
profile/profile.component.ts
import { Component, OnInit } from '@angular/core'; import { TokenStorageService } from '../_services/token-storage.service'; @Component({ selector: 'app-profile', templateUrl: './profile.component.html', styleUrls: ['./profile.component.css'] }) export class ProfileComponent implements OnInit { currentUser: any; constructor(private token: TokenStorageService) { } ngOnInit(): void { this.currentUser = this.token.getUser(); } }
profile/profile.component.html
<div class="container" *ngIf="currentUser; else loggedOut"> <header class="jumbotron"> <h3> <strong>{{ currentUser.username }}</strong> Profile </h3> </header> <p> <strong>Token:</strong> {{ currentUser.accessToken.substring(0, 20) }} ... {{ currentUser.accessToken.substr(currentUser.accessToken.length - 20) }} </p> <p> <strong>Email:</strong> {{ currentUser.email }} </p> <strong>Roles:</strong> <ul> <li *ngFor="let role of currentUser.roles"> {{ role }} </li> </ul> </div> <ng-template #loggedOut> Please login. </ng-template>
Create Role-based Components
Public Component
Our Home Component will use UserService to get public resources from back-end.
home/home.component.ts
import { Component, OnInit } from '@angular/core'; import { UserService } from '../_services/user.service'; @Component({ selector: 'app-home', templateUrl: './home.component.html', styleUrls: ['./home.component.css'] }) export class HomeComponent implements OnInit { content?: string; constructor(private userService: UserService) { } ngOnInit(): void { this.userService.getPublicContent().subscribe({ next: data => { this.content = data; }, error: err => { this.content = JSON.parse(err.error).message; } }); } }
home/home.component.html
<div class="container"> <header class="jumbotron"> <p>{{ content }}</p> </header> </div>
Protected Components
These Components are role-based. But authorization will be processed by back-end.
We only need to call UserService methods:
getUserBoard() getModeratorBoard() getAdminBoard()
Here is an example for BoardAdminComponent.
BoardModeratorComponent & BoardUserComponent are similar.
board-admin/board-admin.component.ts
import { Component, OnInit } from '@angular/core'; import { UserService } from '../_services/user.service'; @Component({ selector: 'app-board-admin', templateUrl: './board-admin.component.html', styleUrls: ['./board-admin.component.css'] }) export class BoardAdminComponent implements OnInit { content?: string; constructor(private userService: UserService) { } ngOnInit(): void { this.userService.getAdminBoard().subscribe({ next: data => { this.content = data; }, error: err => { this.content = JSON.parse(err.error).message; } }); } }
board-admin/board-admin.component.html
<div class="container"> <header class="jumbotron"> <p>{{ content }}</p> </header> </div>
App Routing Module
We configure the Routing for our Angular app in app-routing.module.ts.
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { RegisterComponent } from './register/register.component'; import { LoginComponent } from './login/login.component'; import { HomeComponent } from './home/home.component'; import { ProfileComponent } from './profile/profile.component'; import { BoardUserComponent } from './board-user/board-user.component'; import { BoardModeratorComponent } from './board-moderator/board-moderator.component'; import { BoardAdminComponent } from './board-admin/board-admin.component'; const routes: Routes = [ { path: 'home', component: HomeComponent }, { path: 'login', component: LoginComponent }, { path: 'register', component: RegisterComponent }, { path: 'profile', component: ProfileComponent }, { path: 'user', component: BoardUserComponent }, { path: 'mod', component: BoardModeratorComponent }, { path: 'admin', component: BoardAdminComponent }, { path: '', redirectTo: 'home', pathMatch: 'full' } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
Routes array is passed to the RouterModule.forRoot() method.
We’re gonna use <router-outlet></router-outlet> directive in the App Component where contains navbar and display Components (corresponding to routes) content.
App Component
This component is the root Component of our Angular application, it defines the root tag: <app-root></app-root> that we use in index.html.
app.component.ts
import { Component } from '@angular/core'; import { TokenStorageService } from './_services/token-storage.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { private roles: string[] = []; isLoggedIn = false; showAdminBoard = false; showModeratorBoard = false; username?: string; constructor(private tokenStorageService: TokenStorageService) { } ngOnInit(): void { this.isLoggedIn = !!this.tokenStorageService.getToken(); if (this.isLoggedIn) { const user = this.tokenStorageService.getUser(); this.roles = user.roles; this.showAdminBoard = this.roles.includes('ROLE_ADMIN'); this.showModeratorBoard = this.roles.includes('ROLE_MODERATOR'); this.username = user.username; } } logout(): void { this.tokenStorageService.signOut(); window.location.reload(); } }
First, we check isLoggedIn status using TokenStorageService, if it is true, we get user’s roles and set value for showAdminBoard & showModeratorBoard flag. They will control how template navbar displays its items.
The App Component template also has a Logout button link that call logout() method and reload the window.
app.component.html
<div id="app"> <nav class="navbar navbar-expand navbar-dark bg-dark"> <a href="#" class="navbar-brand">bezKoder</a> <ul class="navbar-nav mr-auto" routerLinkActive="active"> <li class="nav-item"> <a href="/home" class="nav-link" routerLink="home">Home </a> </li> <li class="nav-item" *ngIf="showAdminBoard"> <a href="/admin" class="nav-link" routerLink="admin">Admin Board</a> </li> <li class="nav-item" *ngIf="showModeratorBoard"> <a href="/mod" class="nav-link" routerLink="mod">Moderator Board</a> </li> <li class="nav-item"> <a href="/user" class="nav-link" *ngIf="isLoggedIn" routerLink="user">User</a> </li> </ul> <ul class="navbar-nav ml-auto" *ngIf="!isLoggedIn"> <li class="nav-item"> <a href="/register" class="nav-link" routerLink="register">Sign Up</a> </li> <li class="nav-item"> <a href="/login" class="nav-link" routerLink="login">Login</a> </li> </ul> <ul class="navbar-nav ml-auto" *ngIf="isLoggedIn"> <li class="nav-item"> <a href="/profile" class="nav-link" routerLink="profile">{{ username }}</a> </li> <li class="nav-item"> <a href class="nav-link" (click)="logout()">LogOut</a> </li> </ul> </nav> <div class="container"> <router-outlet></router-outlet> </div> </div>
Run the Angular 13 JWT Authentication project
You can run this App with command:
ng serve.
It configures CORS for port 8081, so you have to run command: ng serve --port 8081 instead.
Conclusion
Today we’ve done so many things from setup Angular 13 Project to write Login and Registration example with Services, Components for Token based Authentication and Authorization with JWT and Web Api. I hope you understand the overall layers of our Angular application, and apply it in your project at ease. Now you can build a front-end app that supports JWT Authentication & Authorization with Angular 13, HttpInterceptor and Router.
Add new comment