Contents
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
//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.