Integrating refresh Tokens in Angular: Ensuring Secure and Seamless Authentication

Faruk taiwo
6 min readJul 29, 2024

--

In today’s world of web development, ensuring a seamless and secure user experience is paramount. One critical aspect of this is managing user authentication efficiently. While authentication and authorization form the backbone of any secure application, maintaining user sessions securely over time requires a deeper understanding. This is where refresh tokens come into play.

In this article, we'll dive into the importance of refresh tokens and provide a step-by-step guide on how to implement them in your Angular applications. Whether you're a seasoned developer or just starting out, this guide will help you enhance your app's security and user experience.

Before we delve into refresh tokens, I recommend checking out my previous article on User Authentication and Authorization in Angular. It covers the basics and will give you a solid foundation to build upon.

Why Refresh Token ?

While access tokens are used to grant users access to protected resources, these tokens have a limited lifespan for security reasons. When an access token expires, the user would typically be required to log in again, which can disrupt the user experience.

This is where refresh tokens come into play. A refresh token is a special token used to obtain a new access token without requiring the user to re-authenticate. Typically access token has a lifespan of minutes or hours while refresh token can go for as long as days.

I won’t bore you with the setup details for an Angular project since that’s a necessary prerequisite. So, let’s dive right into the implementation of refresh tokens proper.

Implementing Refresh token

  1. Authentication Service :

In this section, we’ll create a service to handle authentication and token management in our Angular application. This service will be responsible for logging in users, logging them out, storing tokens, and refreshing tokens when they expire.

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { BehaviorSubject, Observable } from 'rxjs';
import { map, catchError, switchMap } from 'rxjs/operators';

@Injectable({
providedIn: 'root'
})
export class AuthService {
private authUrl = 'http://examples/api/login';
private currentUserSubject: BehaviorSubject<any>;
public currentUser: Observable<any>;


constructor(private http: HttpClient) {
this.currentUserSubject = new BehaviorSubject<any>(JSON.parse(localStorage.getItem('currentUser')));
this.currentUser = this.currentUserSubject.asObservable();
}


public get currentUserValue() {
return this.currentUserSubject.value;
}


login(username: string, password: string) {
return this.http.post<any>(`${this.authUrl}/login`, { username, password })
.pipe(
map((user) =>
{
if (user && user.accessToken && user.refreshToken) {
localStorage.setItem('currentUser', JSON.stringify(user));
this.currentUserSubject.next(user);
}
return user;
})
);
}



logout() {
localStorage.removeItem('currentUser');
this.currentUserSubject.next(null);
}



refreshToken() {
return this.http.post<any>(`${this.authUrl}/refresh-token`, {
refreshToken: this.currentUserValue.refreshToken
}).pipe(
map((user) => {
if (user && user.accessToken) {
const currentUser = this.currentUserValue;
currentUser.accessToken = user.accessToken;
localStorage.setItem('currentUser', JSON.stringify(currentUser));
this.currentUserSubject.next(currentUser);
}
return user;
}),
catchError((error) => {
this.logout();
throw error;
})
);
}
}
  • currentUserSubject is a BehaviorSubject that holds the current user's information, while currentUser is an Observable that components can subscribe to in order to get the current user's state. The constructor initializes the currentUserSubject with data from localStorage, if available.
  • The login method makes a POST request to the login endpoint with the user's credentials, and if the response contains accessToken and refreshToken, the user data is stored in localStorage and currentUserSubject is updated. The logout method logs out the user by removing their data from localStorage and setting currentUserSubject to null.
  • The refreshToken method makes a POST request to the refresh token endpoint with the current refresh token, and if a new access token is returned, it updates the current user data and localStorage. If an error occurs, it logs the user out.

2. Interceptor Service:

This Service (JwtInterceptor) intercepts outgoing HTTP requests and adds the JWT to the Authorization header before sending them to the server.

import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { AuthService } from './auth.service';

export class JwtInterceptor implements HttpInterceptor {

constructor(private authSerice: Authservice) {}


intercept(req: HttpRequest<any>, next: HttpHandler) {

let currentUser = this.authService.currentUserValue

if (currentUser && currentUser.accessToken) {
const authReq = req.clone({
setHeaders: {
Authorization: `Bearer ${currentUser.accessToken}`
}
});
return next.handle(authReq);
} else {
return next.handle(req);
}
}
}

The constructor injects the AuthService, and the intercept method retrieves the current user's value from the AuthService. If the user has an accessToken, it clones the request, adds the Authorization header with the token, and forwards the modified request. If no token is present, it forwards the original request.

3. Handling Token Expirations and Refresh Token:

In this section , the intercept method adds the access token to the request headers if available, and catches 401 errors to handle token expiration. The addToken method clones the request and adds the Authorization header with the access token. The handle401Error method handles 401 errors by refreshing the token if it's not already refreshing, updating the refreshTokenSubject, and retrying the request with the new token. If the token refresh fails, it logs the user out.

import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError, BehaviorSubject } from 'rxjs';
import { catchError, filter, take, switchMap } from 'rxjs/operators';
import { AuthService } from './auth.service';

@Injectable()
export class TokenInterceptor implements HttpInterceptor {

private isRefreshing = false;

private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);

constructor(private authService: AuthService) {}


intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
let currentUser = this.authService.currentUserValue;
if (currentUser && currentUser.accessToken) {
request = this.addToken(request, currentUser.accessToken);
}

return next.handle(request).pipe(
catchError(error => {
if (error instanceof HttpErrorResponse && error.status === 401) {
return this.handle401Error(request, next);
} else {
return throwError(error);
}
})
);
}


private addToken(request: HttpRequest<any>, token: string) {
return request.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
}



private handle401Error(request: HttpRequest<any>, next: HttpHandler) {

if (!this.isRefreshing) {
this.isRefreshing = true;
this.refreshTokenSubject.next(null);

return this.authService.refreshToken().pipe(
switchMap((user: any) => {
this.isRefreshing = false;
this.refreshTokenSubject.next(user.accessToken);
return next.handle(this.addToken(request, user.accessToken));
}),
catchError((err) => {
this.isRefreshing = false;
this.authService.logout();
return throwError(err);
})
);
} else {
return this.refreshTokenSubject.pipe(
filter(token => token != null),
take(1),
switchMap(accessToken => {
return next.handle(this.addToken(request, accessToken));
})
);
}
}
}

Firstly , We import necessary Angular modules and RxJS operators. HttpInterceptor allows us to intercept and modify HTTP requests. HttpErrorResponse is used to handle HTTP errors. BehaviorSubject, Observable, and RxJS operators help manage the token refresh process. isRefreshing variable stands as a flag that indicates if a token refresh is in progress, and refreshTokenSubject, a BehaviorSubject to manage the new token once it's refreshed.

  • The next.handle(request) sends the modified request to the server. If an error occurs, the catchError operator checks if the error is an HttpErrorResponse with a 401 status, indicating an unauthorized request. If so, it calls handle401Error to refresh the token. Otherwise, it throws the error.
  • The handle401Error handles 401 errors by checking if a token refresh is already in progress. If not, it sets isRefreshing to true and sets refreshTokenSubject to null. It then calls authService.refreshToken to get a new token. Upon success, it updates refreshTokenSubject with the new token and retries the original request with the updated token. If the token refresh fails, it logs the user out and throws the error. If a token refresh is already in progress, it waits for the new token to be available (filter and take), then retries the original request with the new token.

Conclusion

We explored key concepts including managing access and refresh tokens, utilizing Angular’s HttpInterceptor for token handling, and addressing token expiration errors.

By applying the strategies discussed, such as intercepting requests, handling token refreshes, and managing errors, you ensure that users remain authenticated seamlessly. This approach not only improves user satisfaction but also strengthens your application’s security.

Thank you for reading! We hope this guide has been helpful. If you have any questions or feedback, please feel free to share in the comments.

Happy coding!

Well, if you find this quite educative do follow me on linkedin

--

--