While working on authentication in Angular, one thing I found particularly useful was using an HTTP interceptor to handle authentication automatically.
Instead of manually adding the access token to every API request, the interceptor can:
Attach the JWT access token to outgoing requests
Detect 401 Unauthorized responses
Use the refresh token to obtain a new access token
Retry the failed request automatically
Logout the user if the refresh token is expired/invalid
Here's the approach I used.
- Adding the Access Token to Requests
First, I retrieve the authentication data from localStorage:
const getAuth = () => {
const stored = localStorage.getItem('auth');
return stored ? JSON.parse(stored) : null;
};
const auth = getAuth();
let authReq = req;
If an access token exists, I clone the request and add the Authorization header:
if (auth?.accessToken) {
authReq = req.clone({
setHeaders: {
Authorization: `Bearer ${auth.accessToken}`,
},
});
}
Now every API request automatically contains:
Authorization: Bearer
This means I don't need to manually add the token in every service method.
- Handling 401 Unauthorized
The access token is usually short-lived for security reasons.
Eventually, it can expire and the API may respond with:
401 Unauthorized
Instead of immediately logging the user out, I check whether a refresh token is available:
if (error.status === 401 && auth?.refreshToken)
Then I call my refresh-token endpoint:
return authService.refreshToken().pipe
The backend validates the refresh token and returns a new access token.
- Retrying the Failed Request
Once I receive the new tokens, I store them:
switchMap((newTokens: any) => {
authService.setUser(newTokens);
Then I create a new version of the original request using the new access token:
const retryReq = req.clone({
setHeaders: {
Authorization: Bearer ${newTokens.accessToken},
},
});
return next(retryReq);
So the flow becomes:
API Request
↓
Access Token
↓
Server
↓
401 Unauthorized
↓
Refresh Token
↓
New Access Token
↓
Retry Original Request
The user doesn't have to manually log in again.
- What Happens If Refresh Fails?
The refresh token itself can also expire or become invalid.
In that situation, the refresh request will fail.
I handle that using another catchError:
catchError((refreshError) => {
authService.clearUser();
router.navigate(['/login']);
return throwError(() => refreshError);
})
The authentication data is removed and the user is redirected to the login page.
- Important: Don't Refresh the Refresh Request
There is one edge case we need to handle.
Suppose /Auth/Refresh itself returns 401.
If our interceptor tries to refresh that request again, we could end up with an infinite loop.
So I check:
const isRefreshRequest = req.url.includes('/Auth/Refresh');
If the failed request is already the refresh request, I immediately logout:
if (isRefreshRequest) {
authService.clearUser();
router.navigate(['/login']);
return throwError(() => error);
}
This prevents the interceptor from repeatedly trying to refresh an already-invalid refresh token.
Putting everything together:
import { HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from '../service/auth-service';
import { Router } from '@angular/router';
import { catchError, switchMap, throwError } from 'rxjs';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
const router = inject(Router);
const getAuth = () => {
const stored = localStorage.getItem('auth');
return stored ? JSON.parse(stored) : null;
};
const auth = getAuth();
let authReq = req;
if (auth?.accessToken) {
authReq = req.clone({
setHeaders: {
Authorization: `Bearer ${auth.accessToken}`,
},
});
}
return next(authReq).pipe(
catchError((error: HttpErrorResponse) => {
const isRefreshRequest = req.url.includes('/Auth/Refresh');
if (isRefreshRequest) {
authService.clearUser();
router.navigate(['/login']);
return throwError(() => error);
}
if (error.status === 401 && auth?.refreshToken) {
return authService.refreshToken().pipe(
switchMap((newTokens: any) => {
authService.setUser(newTokens);
const retryReq = req.clone({
setHeaders: {
Authorization: `Bearer ${newTokens.accessToken}`,
},
});
return next(retryReq);
}),
catchError((refreshError) => {
authService.clearUser();
router.navigate(['/login']);
return throwError(() => refreshError);
}),
);
}
return throwError(() => error);
}),
);
};
The main advantage is that authentication logic stays in one place.
Without an interceptor, every API service might need to deal with:
Get token
↓
Add Authorization header
↓
Make request
↓
Check 401
↓
Refresh token
↓
Retry request
With an interceptor, the individual services can stay focused on their actual responsibilities and The interceptor takes care of authentication automatically.
Top comments (0)