How to Implement JWT in Core API and Angular — Part 2

  1. How to Implement JWT in Core API and Angular – Part 1
  2. How to Implement JWT in Core API and Angular — Part 2

1. Introduction

We have introduced how to generate the JWT on the server side in previous article, so we will continue to introduce how to handle the JWT in client-side with Angular in this article.

For demonstration, we need to create a simple login page to call the login API, and create another simple page for checking the login token whether is valid, OK, let’s do it!

2. Create the Models

I will base it on the previous demo project for describing. To communicate with the API, we need to create the corresponding models to map the result, so we create a login-request to pass the login data

Do you want to be a good trading in cTrader?   >> TRY IT! <<
//MyDemo.Client\src\app\models\login-request.ts

export interface LoginRequest {
  username: string;
  password: string;
}

and create a token model

//MyDemo.Client\src\app\models\token.ts

export interface Token {
  [prop: string]: any;

  //the Jwt token return from API after login successfully
  access_token: string;
  //the current user id
  user_id?: string;
  //should be just handle the 'Bearer' type in this sample
  token_type?: string;
  //How long will be the token expired(e.g. after 30 mins). This is a timestamp format
  expires_in?: number;
  //the actually expire time, so should be the expires_in + current time, e.g. 
  //if expires_in = 30 mins, then exp would be current time + 30 mins
  exp?: number;
}

3. Create the Services

There are 3 services we need to handle:

3.1. Handle Token

First, we need to create a jwt-token to handle the token logic, put these logics into a core folder

//MyDemo.Client\src\app\core\jwt-token.ts

import { Token } from "../models/token";
import { capitalize, currentTimestamp } from "./util";

export class JwtToken {
  constructor(protected attributes: Token) {}

  get access_token(): string {
    return this.attributes.access_token;
  }

  get user_id(): string {
    return this.attributes.user_id ?? '';
  }

  get token_type(): string {
    return this.attributes.token_type ?? 'bearer';
  }

  get exp(): number | void {
    return this.attributes.exp;
  }

  valid(): boolean {
    return this.hasAccessToken() && !this.isExpired();
  }

  getBearerToken(): string {
    return this.access_token
      ? [capitalize(this.token_type), this.access_token].join(' ').trim()
      : '';
  }

  private hasAccessToken(): boolean {
    return !!this.access_token;
  }

  /**
  Check the expired time
  Unit: seconds
  */
  private isExpired(): boolean {
    return this.exp !== undefined && this.exp - currentTimestamp() <= 0;
  }

}

create the util for the common helper methods

//MyDemo.Client\src\app\core\util.ts

/**
 * Capitalize first letter
 * @param text the text wants to be capitalized
 * @returns
 */
export function capitalize(text: string): string {
  return text.substring(0, 1).toUpperCase() + text.substring(1, text.length).toLowerCase();
}

/**
 * Get the current timestamp
 * @returns
 */
export function currentTimestamp(): number {
  return Math.ceil(new Date().getTime() / 1000);
}

/**
 * Filter the Non null object to make sure the object is valid
 * @param obj filter object
 * @returns
 */
export function filterObject<T extends Record<string, unknown>>(obj: T) {
  return Object.fromEntries(
    Object.entries(obj).filter(([, value]) => value !== undefined && value !== null)
  );
}

because we need to save the token into local storage, so create a simple local storage service

//MyDemo.Client\src\app\services\local-storage.service.ts

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class LocalStorageService {
  get(key: string) {
    return JSON.parse(localStorage.getItem(key) || '{}') || {};
  }

  set(key: string, value: any): boolean {
    localStorage.setItem(key, JSON.stringify(value));

    return true;
  }

  has(key: string): boolean {
    return !!localStorage.getItem(key);
  }

  remove(key: string) {
    localStorage.removeItem(key);
  }

  clear() {
    localStorage.clear();
  }
}

in the end, create a token service to put these together

//MyDemo.Client\src\app\services\token.service.ts

import { Injectable, OnDestroy } from '@angular/core';
import { Token } from '../models/token';
import { LocalStorageService } from './local-storage.service';
import { JwtToken } from '../core/jwt-token';
import { currentTimestamp, filterObject } from '../core/util';

@Injectable({
  providedIn: 'root',
})
export class TokenService implements OnDestroy {
  private key = 'MyDemo-token';

  private _token?: JwtToken;

  constructor(private store: LocalStorageService) {}

  private get token(): JwtToken | undefined {
    if (!this._token) {
      this._token = new JwtToken(this.store.get(this.key));
    }

    return this._token;
  }

  set(token?: Token): TokenService {
    this.save(token);

    return this;
  }

  clear(): void {
    this.save();
  }

  valid(): boolean {
    return this.token?.valid() ?? false;
  }

  getUserid(): string {
    return this.token?.user_id ?? '';
  }

  getBearerToken(): string {
    return this.token?.getBearerToken() ?? '';
  }

  ngOnDestroy(): void {
  }

  /**
   * Save the token to local storage
   * @param token token model
   */
  private save(token?: Token): void {

    this._token = undefined;

    if (!token) {
      this.store.remove(this.key);
    } else {
      const value = Object.assign({ access_token: '', token_type: 'Bearer' }, token, {
        exp: token.expires_in ? currentTimestamp() + token.expires_in : null,
      });
      this.store.set(this.key, filterObject(value));
    }
  }
}

3.2. Authentication Service

We will call the API to login the system and get the Jwt token, so we need an auth service to handle the login and logout logic

import { Injectable } from '@angular/core';
import { map, tap } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';
import { config } from 'src/assets/config';
import { Token } from '../models/token';
import { TokenService } from './token.service';
import { Router } from '@angular/router';

@Injectable({
  providedIn: 'root',
})
export class AuthService {

  constructor(
    private tokenService: TokenService, private router: Router,
    protected http: HttpClient) {}

    /**
     * Call the API to login
     * @param username user name
     * @param password password
     * @returns Jwt token if login successfully
     */
    login(username: string, password: string) {

      var url = config.apiUrl + "/auth/login";
      //call the API to get token after login successfully
      return this.http.post<Token>(url, { username, password }).pipe(
        tap(token => {
          console.log('auth service logined ', token);
          //save the token into local storage
          this.tokenService.set(token);
        }),
        map(() => {
          console.log('auth service logined and map ', this.check());
          //check the token whether is valid
          return this.check();
        })
      );
    }

    /**
     * Clear the token after logout
     */
    logout(){
      this.tokenService.clear();
      this.router.navigateByUrl('/login');
    }

    check() {
      return this.tokenService.valid();
    }
}

4. Create the Login page

After creating the essential models and services, we can create the login page now. Run the below command to create a login page

ng g c Login --skip-tests

Cause we are just for demonstration, so only need to create a simple login layout :

<!-- MyDemo.Client\src\app\login\login.component.html -->

<p>Login</p>
<div class="row">
  <form class="form-field-full" [formGroup]="loginForm">
  <div class="col-sm-12">
      <mat-form-field class="col-sm-3">
        <mat-label>User Name: </mat-label>
        <input matInput formControlName="username" placeholder="User Name">
      </mat-form-field>
  </div>
  <div  class="col-sm-12">
      <mat-form-field class="col-sm-3">
        <mat-label>Password: </mat-label>
        <input matInput formControlName="password" type="password" placeholder="Password">
      </mat-form-field>

  </div>
  <div  class="col-sm-12">
    <button class="m-r-8 bg-green-700 text-light" mat-raised-button  (click)="login()">Login</button>
  </div>
  <div class="col-sm-12">
    <span style="color: red;" *ngIf="errorMsg != ''">{{ errorMsg }}</span>
  </div>
</form>
</div>

we can use the FormBuilder to get the input values and pass them to the login function

//MyDemo.Client\src\app\login\login.component.ts

import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from '../services/auth.service';
import { FormBuilder } from '@angular/forms';
import { filter } from 'rxjs/operators';
import { HttpErrorResponse } from '@angular/common/http';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.scss']
})
export class LoginComponent {

  constructor(private fb: FormBuilder, 
          private router: Router, 
          private auth: AuthService) {
  }

  public errorMsg: string = '';

  //init the loginForm 
  loginForm = this.fb.nonNullable.group({
    username: '',
    password: '',
  });

  get username() {
    return this.loginForm.get('username')!;
  }

  get password() {
    return this.loginForm.get('password')!;
  }

  login() {
    this.auth
      .login(this.username.value, this.password.value)
      .pipe(filter(authenticated => authenticated))
      .subscribe(
        () => {
          //redirect to user management page if login successfully
          this.router.navigateByUrl('/user-management');
        },
        (errorRes: HttpErrorResponse) => {
          //otherwise then update the error message in the page
          if(errorRes.status == 401){
            this.errorMsg = 'User name or password is not valid!';
          }
            console.log('Error', errorRes);
        }
      );
  }
}

Ok, we can take a look at the result :

1) Input the user name and password and click login

2) It will return the token (can find it in local storage ) and redirect to the user management page

Seems great, right? 🙂

But please hold on, we still have some problems. You will find that if you access the user management page directly and it also can be successful, that means the page didn’t check the login token, that does not make sense.

5. Add the guard for the user pages

We can solve the above issue with guard in Angular. The guard in Angular refers to route guards, which are interfaces that allow you to control navigation and access to routes in your Angular application. route guards allow you to check if a user can activate or deactivate a route, by implementing CanActivate or CanDeactivate interfaces. You can find more details here.

Create the route guards as below:

//MyDemo.Client\src\app\core\auth.guard.ts

import { Injectable } from '@angular/core';
import {
  ActivatedRouteSnapshot,
  CanActivate,
  CanActivateChild,
  RoutesRecognized,
  Router,
  RouterStateSnapshot,
  UrlTree,
} from '@angular/router';
import { filter, pairwise } from 'rxjs/operators';
import { AuthService } from '../services/auth.service';

@Injectable({
  providedIn: 'root',
})
export class AuthGuard implements CanActivate, CanActivateChild {

  previousUrl!: string;
  currentUrl!: string;

  constructor(private auth: AuthService, private router: Router) {
    this.router.events
      .pipe(filter((evt: any) => evt instanceof RoutesRecognized), pairwise())
      .subscribe((events: RoutesRecognized[]) => {
        this.previousUrl = events[0].urlAfterRedirects;
        this.currentUrl = events[1].urlAfterRedirects;
      });
  }

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
    return this.authenticate();
  }

  canActivateChild(
    childRoute: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): boolean | UrlTree {
    return this.authenticate();
  }

  private authenticate(): boolean | UrlTree {
    //check whether is login successfully
    if (this.auth.check()) {
      return true;
    }
    else{
      this.router.navigateByUrl('/login');
    }
    return false;
  }
}

and use it in app routing, add the canActivate in the routes

//MyDemo.Client\src\app\app-routing.module.ts

const routes: Routes = [
  { path: 'user-management', component: UserManagementComponent, canActivate: [AuthGuard] },
  { path: 'login', component: LoginComponent },
];

after that, logout of the page and try to access the user management page directly, you will find that will be auto redirected to the login page!

6. Create the Interceptor

We are almost done, but still, there is an issue that needs to be solved! Even if we can login successfully and redirect to the user management page, we still can’t get the user data, because there is an authorized checking with the user controller API, so we need to pass the token to the API when we get the user data.

We can append the HTTP header with Authorization when calling the get user API /api/users, but we also need to do that for every API, so this is not a good approach!

The best way that we can use the interceptor.

Interceptors in Angular are services that allow you to intercept and transform HTTP requests and responses between your application and the server. Request interceptors can modify headers, add authentication tokens, log requests, etc.

Create the token-interceptor as below

//MyDemo.Client\src\app\core\token-interceptor.ts

import { Injectable } from '@angular/core';
import {
  HttpErrorResponse,
  HttpEvent,
  HttpHandler,
  HttpInterceptor,
  HttpRequest,
} from '@angular/common/http';
import { Router } from '@angular/router';
import { Observable, throwError } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { TokenService } from '../services/token.service';

@Injectable()
export class TokenInterceptor implements HttpInterceptor {
  constructor(
    private tokenService: TokenService,
    private router: Router
  ) {}

  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    const handler = () => {
      //check the url if login then just redirect to user management page after login     
      if (this.router.url.includes('/login')) {
        this.router.navigateByUrl('/user-management');
      }
    };

    if (this.tokenService.valid()) {
      //if the token is valid, then append to the http header for each request
      return next
        .handle(
          request.clone({            
            headers: request.headers.append('Authorization', this.tokenService.getBearerToken()),
            withCredentials: true,
          })
        )
        .pipe(
          catchError((error: HttpErrorResponse) => {
            //error handler
            if (error.status === 401) {
              this.tokenService.clear();
            }
            return throwError(error);
          }),
          tap(() =>{
            handler();})
        );
    }

    return next.handle(request).pipe(tap(() =>{
        handler();
      }));
  }
}

add a provider in app.module

@NgModule({
  ...
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: TokenInterceptor, multi: true },
  ],
  ...
})

after that, the TokenInterceptor will auto append the token in http header for each request.

login to the page again, you will see all of the user data as well!

7. Conclusion

Jwt is a better way to handle the API authorization, after this article, we learned how to handle the Jwt token in Angular, the flow should be as below:

1) Call API login function and get the token after successfully
2) Save the token in local storage
3) Add the checking for each user’s pages
4) Pass the token to each API request to get data

In the end, don’t forget there are two main points that create the router guard for checking login token and an interceptor for sending token to API.

Loading

Views: 18
Total Views: 556 ,

Leave a Reply

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