Integrating refresh Tokens in Angular: Ensuring Secure and Seamless Authentication
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
- 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 aBehaviorSubject
that holds the current user's information, whilecurrentUser
is anObservable
that components can subscribe to in order to get the current user's state. The constructor initializes thecurrentUserSubject
with data fromlocalStorage
, if available.- The
login
method makes a POST request to the login endpoint with the user's credentials, and if the response containsaccessToken
andrefreshToken
, the user data is stored inlocalStorage
andcurrentUserSubject
is updated. Thelogout
method logs out the user by removing their data fromlocalStorage
and settingcurrentUserSubject
tonull
. - 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 andlocalStorage
. 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, thecatchError
operator checks if the error is anHttpErrorResponse
with a401
status, indicating an unauthorized request. If so, it callshandle401Error
to refresh the token. Otherwise, it throws the error. - The
handle401Error
handles401
errors by checking if a token refresh is already in progress. If not, it setsisRefreshing
totrue
and setsrefreshTokenSubject
tonull
. It then callsauthService.refreshToken
to get a new token. Upon success, it updatesrefreshTokenSubject
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
andtake
), 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