In this article, I will implement and explain the complete angular project feature with the login, HTTP interceptors(Auth, Error), role-base guard, protected route. I am going to use StoreRestApi fake API login, refresh token.
Roadmap…
- Implement login route with auth module
- Implement http interceptor
- Implement auth guard
- Implemenet user protected route
#1. Setting up a new project
Start new project
ng new angular-jwt-auth
Add HttpClientModule for in app/root module
In the src/app.module.ts
file, add HttpClientModile
import { HttpClientModule } from '@angular/common/http'; import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent, ], imports: [ BrowserModule, AppRoutingModule, HttpClientModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
Add API Base URL
We are going to use storerestapi.com which provides fake store APIs including JWT login, register, refresh token also. Let’s add Base Url
In the src/envirment/envirment.ts file
declare object property apiBaseUrl
export const environment = { production: false, apiBaseUrl: 'https://api.storerestapi.com' };
For production, add also in src/envirment/envirment.prod.ts file
export const environment = { production: true, apiBaseUrl: 'https://api.storerestapi.com' };
#2. Implement login form with auth module
Setting up AuthModule with routing
Generate auth module
ng g m auth --routing
Add auth module to project as lazy module
In the app-routing.module.ts file
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; const routes: Routes = [ { path: 'auth', loadChildren: () => import('./auth/auth.module').then(m => m.AuthModule) } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
Add ReactiveFormsModule
inside AuthModule
For login, we will be using a form that will be built with angular ReactiveForms. Learn More ReactiveForms Example
In src/app/auth/auth.module file add ReactiveFormsModule
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { AuthRoutingModule } from './auth-routing.module'; import { ReactiveFormsModule } from '@angular/forms'; @NgModule({ declarations: [], imports: [ CommonModule, AuthRoutingModule, ReactiveFormsModule, ] }) export class AuthModule { }
AuthService
AuthService is used for login and logout in this angular app. AuthService notifies other components and services requester logged or not to provide protected routes or resources access.
So how will it work? It stores requester data when the user is logged in and will remove the requester data when the requester will be logged out.
userDataSubject
BehaviorSubject helps to store and manage user data including access and refresh tokens. And userData$
define as an observable which is BehaviorSubject instance observable. userData$ helps to share requester data between components. get userData()
the method as a property that container logged requester data or null
login() method sends
user credential to login API via HTTP post method. and return Observable of API response. In meantime, If the credential is valid and the login is successful, It will store access_token, refresh_tokens, user Information (which is included in token) in userDataSubject
and localStorage.
logout()
method remove userData
from userDataSubject
and localStorage. Removing userData
from userDataSubject
means notifying other components, the user has logged out. Note: If you want to block refresh_token you must call HTTP request
There is one question? If we store user data in userDataSubject
BehaviorSubject in the application. So why do we need to store user data in localStorage?
Let’s explain:
If the user is already logged in and the user closes or refreshes the window, The userDataSubject
value will be null. So the user needs to login again. This is not good for user experiences. So the AuthService of the constructor()
initially checks the user data that exists in localStorage. if exists that means the user already logged in. In the meantime, localStorage user data is stored in userDataSubject
by BehaviorSubject next method.
generateNewTokens()
the method sends refresh_token via HTTP post method. If refrest_token is valid, the system generates new access and refresh token.
isAuthenticated is method as property. which is return boolean value depends on user logged in or not;
isAuthTokenValid() is pure function which validate JWT tokens, and return boolean value against token validity.
getUserDataFromToken() is also pure method which return JWT token payload data.
Note: In this example I used localStorage but there is more couple option like (sessionStorage, Cookies). Better to use localStorage.
In the auth/auth.service.ts
file
import { HttpClient, HttpEvent } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs'; import { environment } from 'src/environments/environment'; import { map } from 'rxjs/operators'; import jwtDecode from 'jwt-decode'; @Injectable({ providedIn: 'root' }) export class AuthService { ACCESS_TOKEN = 'access_token'; REFRESH_TOKEN = 'refresh_token'; private userDataSubject: BehaviorSubject<any> = new BehaviorSubject(null); userData$: Observable<any> = this.userDataSubject.asObservable(); constructor(private http: HttpClient) { if (localStorage.getItem(this.ACCESS_TOKEN) && localStorage.getItem(this.REFRESH_TOKEN)) { const access_token = (<string>localStorage.getItem(this.ACCESS_TOKEN)); const refresh_token = (<string>localStorage.getItem(this.REFRESH_TOKEN)) this.userDataSubject.next({access_token, refresh_token, userInfo: this.getUserDataFromToken(access_token)}) } } get userData(): any { // return userData(userInfo, access_token, refresh_token) or null return this.userDataSubject.value } login(email: string, password: string): Observable<any> { return this.http.post(`${environment.apiBaseUrl}/auth/login`, { email, password }).pipe( map((res: any) => { const access_token = res?.data?.access_token; const refresh_token = res?.data?.refresh_token; this.userDataSubject.next({access_token, refresh_token, userInfo: this.getUserDataFromToken(access_token)}); localStorage.setItem(this.ACCESS_TOKEN, access_token) localStorage.setItem(this.REFRESH_TOKEN, refresh_token) return res }) ) } logout(): void { localStorage.removeItem(this.ACCESS_TOKEN); localStorage.removeItem(this.REFRESH_TOKEN); this.userDataSubject.next(null); // Call http logout method for block refresh token } generateNewTokens(): Observable<HttpEvent<any>> { const refresh_token = this.userDataSubject.value?.refresh_token; return this.http.post(`${environment.apiBaseUrl}/auth/refresh`, { refresh_token }).pipe( map((res: any) => { const access_token = res?.data?.access_token; const refresh_token = res?.data?.refresh_token; this.userDataSubject.next({access_token, refresh_token, userData: this.getUserDataFromToken(access_token)}); localStorage.setItem(this.ACCESS_TOKEN, access_token); localStorage.setItem(this.REFRESH_TOKEN, refresh_token); return res }) ) } get isAuthenticated(): boolean { const refresh_token = this.userDataSubject.value?.refresh_token; if (!refresh_token) { return false } return this.isAuthTokenValid(refresh_token) } isAuthTokenValid(token: string): boolean { const decoded: any = jwtDecode(token); // default decoded exp format is second const expMilSecond: number = decoded?.exp * 1000; // milliseconds const currentTime = Date.now(); // milliseconds if (expMilSecond < currentTime) { return false; } return true; } getUserDataFromToken(token: string): any { const decoded: any = jwtDecode(token); return decoded.data } }
Login page
Generate login component
ng g c auth/login
Add login page route, in the auth-routing.module.ts
file
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { LoginComponent } from './login/login.component'; const routes: Routes = [ { path: 'login', component: LoginComponent } ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule] }) export class AuthRoutingModule { }
Build angular login form
We will use a minimal login form that can be login through APIs but it will not be a good styled form.
In the login.component.ts
file
import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Observable } from 'rxjs'; import { AuthService } from '../auth.service'; @Component({ selector: 'app-login', templateUrl: './login.component.html', styleUrls: ['./login.component.scss'] }) export class LoginComponent implements OnInit { loginForm!: FormGroup; requestData$!: Observable<any>; constructor(private fb: FormBuilder, private authService: AuthService) { } ngOnInit(): void { this.requestData$ = this.authService.userData$; this.loginForm = this.fb.group({ email: ['', [Validators.required, Validators.email]], password: ['', Validators.required] }) } onFormSubmit(): void { const formData: any = this.loginForm.value; this.authService.login(formData?.email, formData?.password) .subscribe((res: any) => { console.log(res) // handle success message and redirect to next page }, (err) => { console.log(err) // handle invalid user message }); } }
Build login html form
In the login.component.html file
<h3>Login Form</h3> <form [formGroup]="loginForm" (ngSubmit)="onFormSubmit()"> <input formControlName="email" type="email" required placeholder="Email"> <br> <input formControlName="password" type="password" required placeholder="Password"> <br> <button [disabled]="this.loginForm.invalid" type="submit">Submit</button> </form> <div style="margin-top: 20px;"> <strong>Requester Data</strong>: <br> {{ requestData$ | async | json }} </div>
Implement HTTP interceptors
Interceptor can transform the request and send the next interceptor chain before the request goes to the outer. and Interceptor can handle outer request errors.
In angular JWT authenticate base application , I am going implement two interceptors, One is auth interceptor and error interceptor.
generate interceptors
ng g interceptor _services/auth --skip-tests ng g interceptor _services/error --skip-tests
Add interceptors in app module providers
In the app.module.ts file
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; ... import { AuthInterceptor } from './_services/auth.interceptor'; import { ErrorsInterceptor } from './_services/errors.interceptor'; @NgModule({ ... providers: [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, { provide: HTTP_INTERCEPTORS, useClass: ErrorsInterceptor, multi: true } ], ... }) export class AppModule { }
Auth Interceptor
Auth interceptor responsible for outer request authentication data (access_token) attach to request headers. Auth interceptor attach access_token to the request and if user access_token goes invalid, it will request for new token via refresh_token and attach to header
So how is going to work?
Stage 1: Auth Inteceptor will check if request is for refresh token which means this HTTP request for new tokens, It’s will pass next to request. Or request is not for refresh_token, it will go stage 2
Stage 2: If request is not for refresh token, then it will check acess_token existence. If access_token exists it’s mean user was logged in. So it will go next stage 3.
If access_token not exists, it will send next interceptor chain.
Stage 3: In this stage, It’s going to checking access_token validity. If access_token valid, It will clone exists request and attach access_token in request headers, Then send to next Interceptor chain. If access_token not valid, it will pass stage 4
Stage 4: Since access_token is not valid, so In this stage, It will going to another refresh_token HTTP request.
In this time, It’s important to understand. Refresh token request is also HTTP request, so it will go interceptor chain also. In the auth.interceptor.ts Stage 1: will be execute of interceptor chain.
If refresh token HTTP request fine and response tokens, It will clone request and attach access_token in request headers then it will go for next interceptor chain. In this time if refresh token response error, error.interceptor will handle it
In the _services/auth.interceptor.ts
file
import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; import { Observable } from 'rxjs'; import { switchMap, take } from 'rxjs/operators'; import { AuthService } from 'src/app/auth/auth.service'; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor(private authService:AuthService) {} intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { // stage 1: Check if request for refresh token if (request.url.indexOf('/auth/refresh') !== -1) { return next.handle(request); } const data = this.authService.getUserData; const accessToken = data?.access_token; // stage 2: Checking access_token exists(mean user logged in) or not if (accessToken) { // stage 3: checking access_token validity if (this.authService.isAuthTokenValid(accessToken)) { let modifiedReq = request.clone({ headers: request.headers.append('Authorization', `Bearer ${accessToken}`) }); return next.handle(modifiedReq) } // stage 4: Going to generate new tokens return this.authService.generateNewTokens() .pipe( take(1), switchMap((res: any) => { let modifiedReq = request.clone({ headers: request.headers.append('Authorization', `Bearer ${res?.data?.access_token}`) }); return next.handle(modifiedReq) }) ) } return next.handle(request); } }
Error interceptor
Error interceptor intercept HTTP error response. When error response is 401 Unauthorized or 403 Forbidden, this application call authService logout method. Which means logout current user. At the end this interceptor return filtered status and message of error.
In the _services/error.interceptor.ts
import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; import { Observable, throwError } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { AuthService } from 'src/app/auth/auth.service'; @Injectable() export class ErrorsInterceptor implements HttpInterceptor { constructor(private authService: AuthService) {} intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { return next.handle(request).pipe(catchError((res) => this.errorHandler(res))); } private errorHandler(response: any): Observable<any> { // console.error('root error res', response) const status = response?.status; if (status === 401 || status === 403) { this.authService.logout(); } const error = response.error; let message = response.message; if (typeof error === 'object') { const keys = Object.keys(error); if (keys.some(item => item === 'message')) { message = error.message; } } else if (typeof error === 'string') { message = error; } return throwError({ message, status }); } }
Implement Guard
Generate angular Role Base Guard
ng g g _guard/roleBase --skip-tests
RoleBase Guard
Stage 1: Role base guard check authentication first, If user is not authenticated, guard send current user to login page and log him out.
Stage 2: In stage two, check role, If role not valid for this route it will send home page for this example. For real case send user to 403 page.
Note: If your user have multiple roles, use below condition…
if (!validRoles.some((r: string) => userData?.userInfo?.role.include(r))) { // ... return false; }
In the _guard/role-base.guard.ts
file
import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, CanActivate, Router, UrlTree } from '@angular/router'; import { Observable } from 'rxjs'; import { AuthService } from '../auth/auth.service'; @Injectable({ providedIn: 'root' }) export class RoleBaseGuard implements CanActivate { constructor(private authService: AuthService, private router: Router) {} canActivate( route: ActivatedRouteSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree { // Stage 1: check user authentication if (!this.authService.isAuthenticated) { this.router.navigate(['/auth/login']); this.authService.logout(); return false; } const validRoles = route.data['authorities'] || []; const userData = this.authService.getUserData; // Stage 2: Check user role // Condition for multiple role // (!validRoles.some((r: string) => userData?.userInfo?.role.include(r))) if (!validRoles.some((r: string) => r === userData?.userInfo?.role)) { // this.router.navigate(['/error/403']); // Best place to send user this.router.navigate(['/']); // For this example case return false; } return true; } }
Implement protected page
I am going to implement a user list page as a protected route
Generate user module
ng g m user --routing
Add UserModule to the app
In the app-routing.module.ts
file
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; const routes: Routes = [ { path: 'auth', loadChildren: () => import('./auth/auth.module').then(m => m.AuthModule) }, { path: 'users', loadChildren: () => import('./user/user.module').then(m => m.UserModule) } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
User page
generate user-list component
ng g c user/userList
Set user list component route with RoleBaseGuard
Attach RoleBaseGuard to user list route. In the data object section define authorities that can be access this page
In the user-routing.module.ts
file
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { RoleBaseGuard } from '../_guards/role-base.guard'; import { UserListComponent } from './user-list/user-list.component'; const routes: Routes = [ { path: 'list', component: UserListComponent, canActivate: [RoleBaseGuard], data: { authorities: ['ROLE_CUSTOMER', 'ROLE_ADMIN'] } } ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule] }) export class UserRoutingModule { }
Checkout code: https://github.com/techincent/angular-jw-auth