Overview
The AuthInterceptor is an Angular HTTP interceptor that automatically injects JWT authentication tokens into outgoing HTTP requests and handles 401 (Unauthorized) errors for session expiry.
Import
import { AuthInterceptor } from 'src/app/core/interceptors/auth.interceptor';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
Implementation
import { AuthService } from './../services/auth.service';
import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { Router } from '@angular/router';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private router: Router, private authService: AuthService) {}
intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
const token = this.authService.getAuthToken();
let authReq = req;
if (token) {
authReq = req.clone({
setHeaders: { Authorization: `Bearer ${token}` }
});
}
return next.handle(authReq).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401 && error.message === 'Session expired. Please login again') {
this.authService.logOut();
this.router.navigate(['/auth/login']);
}
return throwError(() => error);
})
);
}
}
Method
intercept
Intercepts HTTP requests to inject authentication tokens and handle auth errors.
intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>>
req
HttpRequest<unknown>
required
The outgoing HTTP request
The next interceptor in the chain or the backend
return
Observable<HttpEvent<unknown>>
Observable of HTTP events
Behavior:
-
Token Injection:
- Retrieves JWT token from
AuthService
- If token exists, clones the request and adds
Authorization header
- Format:
Authorization: Bearer {token}
-
Error Handling:
- Catches HTTP errors using
catchError
- If error is 401 with “Session expired” message:
- Logs out the user via
AuthService.logOut()
- Redirects to login page
- Re-throws the error for component-level handling
Registration
app.module.ts
import { NgModule } from '@angular/core';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { AuthInterceptor } from './core/interceptors/auth.interceptor';
@NgModule({
imports: [
HttpClientModule,
// other imports
],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
}
]
})
export class AppModule {}
Important: Set multi: true to allow multiple interceptors to be registered. Without it, this interceptor will replace all other interceptors.
Usage Examples
Automatic Token Injection
import { Component } from '@angular/core';
import { FavoritesService } from 'src/app/shared/services/favorites/favorites.service';
@Component({
selector: 'app-favorites',
templateUrl: './favorites.component.html'
})
export class FavoritesComponent {
constructor(private favoritesService: FavoritesService) {}
loadFavorites() {
// No need to manually add Authorization header!
// AuthInterceptor automatically injects the token
this.favoritesService.getFavorites(1, 10).subscribe({
next: (response) => {
console.log('Favorites:', response.favorites);
},
error: (error) => {
// If 401 error with session expired message,
// user is automatically logged out and redirected
}
});
}
}
Before and After Interception
Without AuthInterceptor (manual approach):
// Manual token injection - NOT NEEDED with interceptor
const token = this.authService.getAuthToken();
const headers = new HttpHeaders({
'Authorization': `Bearer ${token}`
});
this.http.get('/api/favorites', { headers }).subscribe(...);
With AuthInterceptor (automatic):
// Token automatically injected - clean code!
this.http.get('/api/favorites').subscribe(...);
Request Flow
Successful Request with Token
- Service makes HTTP request (e.g.,
http.get('/api/favorites'))
- AuthInterceptor intercepts the request
- Retrieves token from
AuthService.getAuthToken()
- Clones request and adds
Authorization: Bearer {token} header
- Passes modified request to next handler
- Backend validates token
- Response returned to service
Request without Token
- Service makes HTTP request
- AuthInterceptor intercepts the request
- No token found in
AuthService
- Request passes through unchanged (no Authorization header)
- Backend may return 401 if authentication is required
Session Expiry (401 Error)
- Service makes HTTP request
- AuthInterceptor adds token
- Backend validates token and detects expiry
- Backend returns 401 Unauthorized with message “Session expired. Please login again”
- AuthInterceptor catches error
- Calls
authService.logOut() to clear session
- Redirects to
/auth/login
- Shows toast notification (via ErrorInterceptor)
- Error re-thrown for component handling
Error Handling
Session Expired (401)
if (error.status === 401 && error.message === 'Session expired. Please login again') {
this.authService.logOut(); // Clear session storage
this.router.navigate(['/auth/login']); // Redirect to login
}
Other 401 Errors
If the 401 error doesn’t match the session expired message, it’s passed through to the component:
this.favoritesService.addToFavorites(mediaItem).subscribe({
error: (error) => {
if (error.status === 401) {
// Handle unauthorized access
console.error('Unauthorized:', error.message);
}
}
});
Testing
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { HTTP_INTERCEPTORS, HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Router } from '@angular/router';
import { AuthInterceptor } from './auth.interceptor';
import { AuthService } from '../services/auth.service';
describe('AuthInterceptor', () => {
let httpMock: HttpTestingController;
let httpClient: HttpClient;
let authService: jasmine.SpyObj<AuthService>;
let router: jasmine.SpyObj<Router>;
beforeEach(() => {
const authServiceSpy = jasmine.createSpyObj('AuthService', ['getAuthToken', 'logOut']);
const routerSpy = jasmine.createSpyObj('Router', ['navigate']);
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
{ provide: AuthService, useValue: authServiceSpy },
{ provide: Router, useValue: routerSpy }
]
});
httpMock = TestBed.inject(HttpTestingController);
httpClient = TestBed.inject(HttpClient);
authService = TestBed.inject(AuthService) as jasmine.SpyObj<AuthService>;
router = TestBed.inject(Router) as jasmine.SpyObj<Router>;
});
afterEach(() => {
httpMock.verify();
});
it('should add Authorization header when token exists', () => {
const token = 'test-token-123';
authService.getAuthToken.and.returnValue(token);
httpClient.get('/api/test').subscribe();
const req = httpMock.expectOne('/api/test');
expect(req.request.headers.has('Authorization')).toBe(true);
expect(req.request.headers.get('Authorization')).toBe(`Bearer ${token}`);
});
it('should not add Authorization header when token is null', () => {
authService.getAuthToken.and.returnValue(null);
httpClient.get('/api/test').subscribe();
const req = httpMock.expectOne('/api/test');
expect(req.request.headers.has('Authorization')).toBe(false);
});
it('should logout and redirect on 401 session expired error', () => {
authService.getAuthToken.and.returnValue('token');
httpClient.get('/api/test').subscribe(
() => fail('should have failed with 401 error'),
(error: HttpErrorResponse) => {
expect(error.status).toBe(401);
expect(authService.logOut).toHaveBeenCalled();
expect(router.navigate).toHaveBeenCalledWith(['/auth/login']);
}
);
const req = httpMock.expectOne('/api/test');
req.flush(
{ message: 'Session expired. Please login again' },
{ status: 401, statusText: 'Unauthorized' }
);
});
});
Multiple Interceptors
AuthInterceptor works alongside other interceptors. They execute in the order they’re registered:
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
},
{
provide: HTTP_INTERCEPTORS,
useClass: ErrorInterceptor, // Runs after AuthInterceptor
multi: true
}
]
Execution Order:
- Request phase: AuthInterceptor → ErrorInterceptor → Backend
- Response phase: Backend → ErrorInterceptor → AuthInterceptor → Component
Security Considerations
Security Best Practices:
-
HTTPS Only: Always use HTTPS in production to protect tokens during transmission.
-
Token Storage: Tokens are stored in session storage (not local storage), which is cleared when the browser closes.
-
Token Expiry: Backend must validate token expiry and return 401 with the specific message.
-
No Token Logging: Never log the actual token value in production code.
-
Selective Injection: Only inject tokens for authenticated API endpoints, not for public resources.
Skipping Interceptor (Optional Enhancement)
import { HttpContext, HttpContextToken } from '@angular/common/http';
// Create a context token
export const SKIP_AUTH = new HttpContextToken<boolean>(() => false);
// In interceptor
intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
// Skip auth for specific requests
if (req.context.get(SKIP_AUTH)) {
return next.handle(req);
}
// Normal auth logic
const token = this.authService.getAuthToken();
// ...
}
// In service
this.http.get('/api/public', {
context: new HttpContext().set(SKIP_AUTH, true)
});