How to implement JWT authentication in Angular
JWT authentication provides stateless, secure authentication using JSON Web Tokens for API authorization, enabling scalable authentication across distributed systems. As the creator of CoreUI, a widely used open-source UI library, I’ve implemented JWT authentication in Angular applications throughout my 12 years of frontend development since 2014. The most robust approach is using HTTP interceptors to automatically attach JWT tokens to requests and handle token refresh for expired tokens. This method centralizes token management, provides automatic authorization headers, and implements seamless token renewal without user intervention.
Install jwt-decode library and create JWT authentication service with token refresh functionality.
npm install jwt-decode
// src/app/services/jwt-auth.service.ts
import { Injectable } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { BehaviorSubject, Observable, throwError } from 'rxjs'
import { tap, catchError, switchMap } from 'rxjs/operators'
import { Router } from '@angular/router'
import { jwtDecode } from 'jwt-decode'
interface JwtPayload {
sub: string
email: string
exp: number
iat: number
}
interface AuthResponse {
accessToken: string
refreshToken: string
}
@Injectable({
providedIn: 'root'
})
export class JwtAuthService {
private accessTokenSubject = new BehaviorSubject<string | null>(null)
public accessToken$ = this.accessTokenSubject.asObservable()
private readonly ACCESS_TOKEN_KEY = 'access_token'
private readonly REFRESH_TOKEN_KEY = 'refresh_token'
constructor(
private http: HttpClient,
private router: Router
) {
const token = this.getAccessToken()
if (token) {
this.accessTokenSubject.next(token)
}
}
login(email: string, password: string): Observable<AuthResponse> {
return this.http.post<AuthResponse>('/api/auth/login', { email, password })
.pipe(
tap(response => this.setTokens(response)),
catchError(error => {
console.error('Login failed:', error)
return throwError(() => error)
})
)
}
refreshToken(): Observable<AuthResponse> {
const refreshToken = this.getRefreshToken()
if (!refreshToken) {
return throwError(() => new Error('No refresh token available'))
}
return this.http.post<AuthResponse>('/api/auth/refresh', { refreshToken })
.pipe(
tap(response => this.setTokens(response)),
catchError(error => {
this.logout()
return throwError(() => error)
})
)
}
logout(): void {
localStorage.removeItem(this.ACCESS_TOKEN_KEY)
localStorage.removeItem(this.REFRESH_TOKEN_KEY)
this.accessTokenSubject.next(null)
this.router.navigate(['/login'])
}
private setTokens(response: AuthResponse): void {
localStorage.setItem(this.ACCESS_TOKEN_KEY, response.accessToken)
localStorage.setItem(this.REFRESH_TOKEN_KEY, response.refreshToken)
this.accessTokenSubject.next(response.accessToken)
}
getAccessToken(): string | null {
return localStorage.getItem(this.ACCESS_TOKEN_KEY)
}
getRefreshToken(): string | null {
return localStorage.getItem(this.REFRESH_TOKEN_KEY)
}
isTokenExpired(token: string): boolean {
try {
const decoded = jwtDecode<JwtPayload>(token)
const currentTime = Date.now() / 1000
return decoded.exp < currentTime
} catch {
return true
}
}
isAuthenticated(): boolean {
const token = this.getAccessToken()
if (!token) {
return false
}
return !this.isTokenExpired(token)
}
getUserFromToken(): JwtPayload | null {
const token = this.getAccessToken()
if (!token) {
return null
}
try {
return jwtDecode<JwtPayload>(token)
} catch {
return null
}
}
}
Create HTTP interceptor for automatic token attachment and refresh:
// src/app/interceptors/jwt.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 { JwtAuthService } from '../services/jwt-auth.service'
@Injectable()
export class JwtInterceptor implements HttpInterceptor {
private isRefreshing = false
private refreshTokenSubject = new BehaviorSubject<string | null>(null)
constructor(private authService: JwtAuthService) {}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
const token = this.authService.getAccessToken()
if (token && !this.isRefreshing) {
request = this.addToken(request, token)
}
return next.handle(request).pipe(
catchError(error => {
if (error instanceof HttpErrorResponse && error.status === 401) {
return this.handle401Error(request, next)
}
return throwError(() => error)
})
)
}
private addToken(request: HttpRequest<unknown>, token: string): HttpRequest<unknown> {
return request.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
})
}
private handle401Error(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
if (!this.isRefreshing) {
this.isRefreshing = true
this.refreshTokenSubject.next(null)
return this.authService.refreshToken().pipe(
switchMap(response => {
this.isRefreshing = false
this.refreshTokenSubject.next(response.accessToken)
return next.handle(this.addToken(request, response.accessToken))
}),
catchError(error => {
this.isRefreshing = false
this.authService.logout()
return throwError(() => error)
})
)
}
return this.refreshTokenSubject.pipe(
filter(token => token !== null),
take(1),
switchMap(token => next.handle(this.addToken(request, token!)))
)
}
}
Create JWT auth guard:
// src/app/guards/jwt-auth.guard.ts
import { Injectable } from '@angular/core'
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'
import { JwtAuthService } from '../services/jwt-auth.service'
@Injectable({
providedIn: 'root'
})
export class JwtAuthGuard implements CanActivate {
constructor(
private authService: JwtAuthService,
private router: Router
) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean {
const token = this.authService.getAccessToken()
if (token && !this.authService.isTokenExpired(token)) {
return true
}
const refreshToken = this.authService.getRefreshToken()
if (refreshToken) {
this.authService.refreshToken().subscribe({
next: () => {
return true
},
error: () => {
this.router.navigate(['/login'], {
queryParams: { returnUrl: state.url }
})
return false
}
})
}
this.router.navigate(['/login'], {
queryParams: { returnUrl: state.url }
})
return false
}
}
Configure interceptor in application config:
// src/app/app.config.ts
import { ApplicationConfig } from '@angular/core'
import { provideRouter } from '@angular/router'
import { provideHttpClient, withInterceptors, HTTP_INTERCEPTORS } from '@angular/common/http'
import { routes } from './app.routes'
import { JwtInterceptor } from './interceptors/jwt.interceptor'
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(),
{
provide: HTTP_INTERCEPTORS,
useClass: JwtInterceptor,
multi: true
}
]
}
Use JWT auth in components:
import { Component, OnInit } from '@angular/core'
import { JwtAuthService } from './services/jwt-auth.service'
import { CommonModule } from '@angular/common'
@Component({
selector: 'app-user-profile',
standalone: true,
imports: [CommonModule],
template: `
<div *ngIf='user'>
<h2>User Profile</h2>
<p>Email: {{ user.email }}</p>
<p>User ID: {{ user.sub }}</p>
<p>Token expires: {{ expirationDate | date:'medium' }}</p>
<button (click)='logout()'>Logout</button>
</div>
`
})
export class UserProfileComponent implements OnInit {
user: any
expirationDate: Date | null = null
constructor(private authService: JwtAuthService) {}
ngOnInit(): void {
this.user = this.authService.getUserFromToken()
if (this.user) {
this.expirationDate = new Date(this.user.exp * 1000)
}
}
logout(): void {
this.authService.logout()
}
}
Here the jwt-decode library safely decodes JWT tokens to extract payload information without verification. The JwtAuthService manages both access and refresh tokens with automatic refresh logic for expired tokens. The isTokenExpired method compares token expiration timestamp with current time to determine validity. The JwtInterceptor automatically attaches Bearer token to all outgoing HTTP requests in Authorization header. The handle401Error method implements token refresh when API returns 401 status for expired tokens. The refreshTokenSubject prevents multiple simultaneous refresh requests using BehaviorSubject pattern. The guard checks token expiration before route activation and attempts refresh if token is expired but refresh token exists.
Best Practice Note:
This is the JWT authentication implementation we use in CoreUI for Angular templates for secure API communication with automatic token management. Store refresh tokens in httpOnly cookies instead of localStorage for enhanced security against XSS attacks, implement token rotation where each refresh generates new refresh token to prevent replay attacks, and add token expiration buffer (refresh 5 minutes before expiration) to prevent race conditions where token expires during API request processing.



