Authentication is a critical part of most modern web applications. In this comprehensive guide, I'll walk you through implementing a secure authentication system in Angular using OAuth with JWT tokens, interceptors, and refresh tokens.
Table of Contents
- Understanding the Authentication Flow
- Setting Up the Angular Project
- Creating Authentication Services
- Implementing JWT Interceptors
- Handling Refresh Tokens
- Protecting Routes with Guards
- Best Practices and Security Considerations
Understanding the Authentication Flow
Before diving into code, let's understand the complete authentication flow we'll implement:
- User logs in with credentials (OAuth flow)
- Server returns JWT access token and refresh token
- Access token is included in subsequent requests
- When access token expires, refresh token is used to get new tokens
- If refresh fails, user is logged out
Setting Up the Angular Project
First, let's create a new Angular project and install necessary dependencies:
ng new angular-auth-demo
cd angular-auth-demo
# Install required packages
npm install @auth0/angular-jwt rxjs jwt-decode
Creating Authentication Services
1. Auth Service
Create an authentication service that will handle login, logout, and token management:
// src/app/services/auth.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, throwError } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';
import * as jwt_decode from 'jwt-decode';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private readonly API_URL = 'https://your-auth-server.com/api';
private currentUserSubject: BehaviorSubject<any>;
public currentUser: Observable<any>;
constructor(private http: HttpClient) {
this.currentUserSubject = new BehaviorSubject<any>(
JSON.parse(localStorage.getItem('currentUser') || null)
);
this.currentUser = this.currentUserSubject.asObservable();
}
public get currentUserValue(): any {
return this.currentUserSubject.value;
}
public get accessToken(): string {
return this.currentUserValue?.accessToken;
}
public get refreshToken(): string {
return this.currentUserValue?.refreshToken;
}
login(username: string, password: string): Observable<any> {
return this.http.post<any>(`${this.API_URL}/login`, { username, password })
.pipe(
map(user => {
// store user details and tokens in local storage
localStorage.setItem('currentUser', JSON.stringify(user));
this.currentUserSubject.next(user);
return user;
}),
catchError(error => {
return throwError(error);
})
);
}
refreshToken(): Observable<any> {
return this.http.post<any>(`${this.API_URL}/refresh-token`, {
refreshToken: this.refreshToken
}).pipe(
tap((tokens) => {
const user = {
...this.currentUserValue,
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken
};
localStorage.setItem('currentUser', JSON.stringify(user));
this.currentUserSubject.next(user);
})
);
}
logout() {
// remove user from local storage and set current user to null
localStorage.removeItem('currentUser');
this.currentUserSubject.next(null);
}
isTokenExpired(token?: string): boolean {
if (!token) token = this.accessToken;
if (!token) return true;
const date = this.getTokenExpirationDate(token);
if (date === undefined) return false;
return !(date.valueOf() > new Date().valueOf());
}
private getTokenExpirationDate(token: string): Date | undefined {
try {
const decoded: any = jwt_decode(token);
if (decoded.exp === undefined) return undefined;
const date = new Date(0);
date.setUTCSeconds(decoded.exp);
return date;
} catch (Error) {
return undefined;
}
}
}
2. User Service
Create a user service to handle user-related operations:
// src/app/services/user.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class UserService {
private readonly API_URL = 'https://your-api-server.com/api';
constructor(private http: HttpClient) { }
getProfile() {
return this.http.get(`${this.API_URL}/profile`);
}
// Add other user-related methods here
}
Implementing JWT Interceptors
Interceptors are powerful tools in Angular for handling HTTP requests and responses globally. We'll create two interceptors: one for adding the JWT token to requests and another for handling token refresh when the access token expires.
1. JWT Interceptor
// src/app/interceptors/jwt.interceptor.ts
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { AuthService } from '../services/auth.service';
@Injectable()
export class JwtInterceptor implements HttpInterceptor {
constructor(private authService: AuthService) {}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
// Skip for auth requests or if no token exists
if (request.url.includes('/login') || request.url.includes('/refresh-token') || !this.authService.accessToken) {
return next.handle(request);
}
// Clone the request and add the authorization header
request = request.clone({
setHeaders: {
Authorization: `Bearer ${this.authService.accessToken}`
}
});
return next.handle(request);
}
}
2. Error Interceptor for Token Refresh
// src/app/interceptors/error.interceptor.ts
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 '../services/auth.service';
@Injectable()
export class ErrorInterceptor 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>> {
return next.handle(request).pipe(
catchError(error => {
if (error instanceof HttpErrorResponse && error.status === 401) {
return this.handle401Error(request, next);
} else {
return throwError(error);
}
})
);
}
private handle401Error(request: HttpRequest<any>, next: HttpHandler) {
if (!this.isRefreshing) {
this.isRefreshing = true;
this.refreshTokenSubject.next(null);
return this.authService.refreshToken().pipe(
switchMap((token: any) => {
this.isRefreshing = false;
this.refreshTokenSubject.next(token.accessToken);
return next.handle(this.addTokenHeader(request, token.accessToken));
}),
catchError((err) => {
this.isRefreshing = false;
this.authService.logout();
return throwError(err);
})
);
} else {
return this.refreshTokenSubject.pipe(
filter(token => token !== null),
take(1),
switchMap((token) => {
return next.handle(this.addTokenHeader(request, token));
})
);
}
}
private addTokenHeader(request: HttpRequest<any>, token: string) {
return request.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
}
}
Registering the Interceptors
Add these interceptors to your app module:
// src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { JwtInterceptor } from './interceptors/jwt.interceptor';
import { ErrorInterceptor } from './interceptors/error.interceptor';
@NgModule({
imports: [
BrowserModule,
HttpClientModule
],
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true }
],
bootstrap: [AppComponent]
})
export class AppModule { }
Protecting Routes with Guards
Angular route guards help protect routes based on certain conditions. Let's create an auth guard:
// src/app/guards/auth.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from '../services/auth.service';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {}
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
if (this.authService.currentUserValue && !this.authService.isTokenExpired()) {
return true;
}
// not logged in or token expired, redirect to login page
this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
return false;
}
}
Use the guard in your routing module:
// src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LoginComponent } from './components/login/login.component';
import { ProfileComponent } from './components/profile/profile.component';
import { AuthGuard } from './guards/auth.guard';
const routes: Routes = [
{ path: 'login', component: LoginComponent },
{ path: 'profile', component: ProfileComponent, canActivate: [AuthGuard] },
{ path: '', redirectTo: '/profile', pathMatch: 'full' }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Best Practices and Security Considerations
Secure Token Storage: While we used localStorage in this example, for higher security applications consider using HttpOnly cookies for tokens.
Short-Lived Access Tokens: Access tokens should have a short lifespan (e.g., 15-30 minutes) while refresh tokens can last longer (e.g., 7 days).
Refresh Token Rotation: Implement refresh token rotation where each refresh request returns a new refresh token and invalidates the old one.
CORS Configuration: Ensure your server is properly configured with CORS to only allow requests from your Angular app's domain.
HTTPS: Always use HTTPS in production to prevent token interception.
Token Revocation: Implement token revocation for logged-out users.
Rate Limiting: Protect your authentication endpoints with rate limiting to prevent brute force attacks.
Input Validation: Always validate user input on both client and server sides.
Conclusion
In this guide, we've implemented a comprehensive authentication system in Angular using OAuth with JWT tokens. We've covered:
- Setting up authentication services
- Implementing JWT interceptors for automatic token handling
- Creating a refresh token mechanism
- Protecting routes with guards
- Following security best practices
This implementation provides a solid foundation for secure authentication in your Angular applications. Remember to adapt it to your specific requirements and always stay updated with the latest security practices.
Happy coding!