Tech Incent
Angular

Angular JWT token authentication (login, auth and error interceptors, guard, protected route) example: Step by step

angular jwt auth

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

Related posts

How to add tailwind CSS in angular

Sajal Mia

Angular lifecycle hooks explanation

Sajal Mia

Angular Production-Ready Project Setup

Sajal Mia

Explained RxJs switchMap operator with example

Sajal Mia

Angular Search and pagination with example

Sajal Mia

Dockerize angular app with example

Sajal Mia