With previous posts, we’ve known how to build JWT Authentication and Authorization in a Angular 16 Application. In this tutorial, I will continue to show you way to implement Angular 16 Refresh Token before Expiration with Http Interceptor and JWT.
Related Posts:
– In-depth Introduction to JWT-JSON Web Token
– Angular 16 JWT Authentication & Authorization example
– Angular 16 Logout when Token is expired
Contents
Overview
The diagram shows flow of how we implement Angular 16 Refresh Token with JWT and Http Interceptor example.
– A refresh Token
will be provided in HttpOnly Cookie at the time user signs in.
– If Angular 16 Client accesses protected resources, a legal JWT
must be stored in HttpOnly Cookie together with HTTP request.
– With the help of Http Interceptor, Angular App can check if the access Token
(JWT) is expired (401), sends /refreshToken
request to receive new access Token
and use it for new resource request.
This is how Refresh Token works in our Angular example:
1- User sends request with legal JWT:
2- JWT is expired, our Application automatically sends Token Refresh request, then uses new Access Token right after that.
The Back-end server for this Angular 16 Client can be found at:
- Spring Boot Refresh Token with JWT
- Node.js Refresh Token with JWT
- Node.js Refresh Token with JWT and MongoDB
We’re gonna implement Token Refresh feature basing on the code from previous post, so you need to read following tutorial first:
Angular 16 JWT Authentication & Authorization example
Add Refresh Token function in Angular Service
Firstly, we need to create refreshToken()
function that uses HttpClient
to send HTTP Request with refreshToken
in the body.
_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) {}
// register, login, logout
refreshToken() {
return this.http.post(AUTH_API + 'refreshtoken', { }, httpOptions);
}
}
Angular 16 Refresh Token with Interceptor
To implement silent refresh JWT token, we need to use an Http Interceptor to check 401
status in the response and call Token Refresh API with the Refresh Token stored in HttpOnly Cookie.
Let’s open _helpers/auth.interceptor.ts and write following code:
import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HTTP_INTERCEPTORS, HttpErrorResponse } from '@angular/common/http';
import { StorageService } from '../_services/storage.service';
import { AuthService } from '../_services/auth.service';
import { Observable, throwError } from 'rxjs';
import { catchError, switchMap } from 'rxjs/operators';
import { EventBusService } from '../_shared/event-bus.service';
import { EventData } from '../_shared/event.class';
@Injectable()
export class HttpRequestInterceptor implements HttpInterceptor {
private isRefreshing = false;
constructor(
private storageService: StorageService,
private authService: AuthService,
private eventBusService: EventBusService
) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
req = req.clone({
withCredentials: true,
});
return next.handle(req).pipe(
catchError((error) => {
if (
error instanceof HttpErrorResponse &&
!req.url.includes('auth/signin') &&
error.status === 401
) {
return this.handle401Error(req, next);
}
return throwError(() => error);
})
);
}
private handle401Error(request: HttpRequest<any>, next: HttpHandler) {
if (!this.isRefreshing) {
this.isRefreshing = true;
if (this.storageService.isLoggedIn()) {
return this.authService.refreshToken().pipe(
switchMap(() => {
this.isRefreshing = false;
return next.handle(request);
}),
catchError((error) => {
this.isRefreshing = false;
if (error.status == '403') {
this.eventBusService.emit(new EventData('logout', null));
}
return throwError(() => error);
})
);
}
}
return next.handle(request);
}
}
export const httpInterceptorProviders = [
{ provide: HTTP_INTERCEPTORS, useClass: HttpRequestInterceptor, multi: true },
];
In the code above, we:
– intercept requests or responses before they are handled by intercept()
method.
– handle 401
status on interceptor response (except response of /signin
request).
– if the user is logged in, call AuthService.refreshToken()
method.
– if the API returns response with 403
error (the refresh token is expired), emit 'logout'
event.
Implement EventBus Service
The logout
event will be dispatched to App
component when response status tells us the access token is expired.
We need to set up a global event-driven system, or a PubSub system, which allows us to listen and dispatch (emit) events from independent components so that they don’t have direct dependencies between each other.
We’re gonna create EventBusService
with two methods: on
and emit
.
_shared/event-bus.service.ts
import { Injectable } from '@angular/core';
import { Subject, Subscription } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { EventData } from './event.class';
@Injectable({
providedIn: 'root'
})
export class EventBusService {
private subject$ = new Subject<EventData>();
constructor() { }
emit(event: EventData) {
this.subject$.next(event);
}
on(eventName: string, action: any): Subscription {
return this.subject$.pipe(
filter((e: EventData) => e.name === eventName),
map((e: EventData) => e["value"])).subscribe(action);
}
}
_shared/event.class.ts
export class EventData {
name: string;
value: any;
constructor(name: string, value: any) {
this.name = name;
this.value = value;
}
}
Now you can emit event
to the bus and if any listener was registered with the eventName
, it will execute the callback function action
.
Next we import EventBusService
in App component and listen to "logout"
event.
src/app.component.ts
import { Component } from '@angular/core';
import { Subscription } from 'rxjs';
import { StorageService } from './_services/storage.service';
import { AuthService } from './_services/auth.service';
import { EventBusService } from './_shared/event-bus.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;
eventBusSub?: Subscription;
constructor(
private storageService: StorageService,
private authService: AuthService,
private eventBusService: EventBusService
) {}
ngOnInit(): void {
this.isLoggedIn = this.storageService.isLoggedIn();
if (this.isLoggedIn) {
const user = this.storageService.getUser();
this.roles = user.roles;
this.showAdminBoard = this.roles.includes('ROLE_ADMIN');
this.showModeratorBoard = this.roles.includes('ROLE_MODERATOR');
this.username = user.username;
}
this.eventBusSub = this.eventBusService.on('logout', () => {
this.logout();
});
}
logout(): void {
this.authService.logout().subscribe({
next: res => {
console.log(res);
this.storageService.clean();
window.location.reload();
},
error: err => {
console.log(err);
}
});
}
}
Finally we only need to emit "logout"
event in the Angular Http Interceptor like previous section.
Conclusion
Today we know how to implement Angular 16 JWT Refresh Token before expiration using Http Interceptor with 401 status code. For your understanding the logic flow, you should read one of following tutorial first:
Angular 16 JWT Authentication & Authorization example
The Back-end server for this Angular 16 Client can be found at:
- Spring Boot Refresh Token with JWT
- Node.js Refresh Token with JWT
- Node.js Refresh Token with JWT and MongoDB
Before running the backend server, you need to add minor configuration:
– Spring Boot:
/* In AuthController.java */
// @CrossOrigin(origins = "*", maxAge = 3600)
@CrossOrigin(origins = "http://localhost:8081", maxAge = 3600, allowCredentials="true")
/* In TestController.java */
// @CrossOrigin(origins = "*", maxAge = 3600)
@CrossOrigin(origins = "http://localhost:8081", maxAge = 3600, allowCredentials="true")
– Node.js Express:
/* In server.js */
// app.use(cors());
app.use(
cors({
credentials: true,
origin: ["http://localhost:8081"],
})
);
They configure CORS for port 8081, so you have to run Angular Client command instead:
ng serve --port 8081
Source Code
You can find the complete source code for this tutorial on Github.
Further Reading
- Angular HttpInterceptor
- In-depth Introduction to JWT-JSON Web Token
- Angular Form Validation example (Reactive Forms)
- Angular CRUD Application example with Web API
- Angular File upload example with Progress bar
- Angular Pagination example | ngx-pagination
Fullstack:
– Angular 16 + Spring Boot: JWT Authentication & Authorization example
– Angular 16 + Node.js Express: JWT Authentication & Authorization example