JWT (JSON Web Token) authentication is a widely used method to secure client-server communication in modern web applications. It allows users to authenticate once and securely transmit information with each subsequent request. JWTs are compact, URL-safe tokens that can be easily used in HTTP headers and efficiently verified by the backend. In this post, we will explore JWT authentication in detail, covering how JWT works, implementing it in Angular, handling tokens, and best practices for secure communication.
1. Introduction to JWT
JWT is a standard for creating tokens that assert claims about a user. The main components of a JWT are:
- Header – contains metadata about the token, typically the signing algorithm.
- Payload – contains user information (claims) and expiration details.
- Signature – a hashed signature that verifies the token’s integrity.
JWTs are stateless, meaning the server does not need to store session information. The token itself carries all the necessary data to authenticate the user.
2. How JWT Authentication Works
- User Login – The user provides credentials (username/password) to the backend.
- Token Generation – If credentials are valid, the backend generates a JWT containing user info and expiration.
- Token Transmission – The JWT is sent to the client, usually in the response body.
- Token Storage – The client stores the JWT in local storage, session storage, or cookies.
- Authenticated Requests – The client includes the JWT in HTTP headers for subsequent API requests.
- Token Verification – The backend verifies the JWT signature and expiration before granting access.
3. JWT Structure
A JWT is typically formatted as:
header.payload.signature
Example JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VybmFtZSI6ImpvaG5kb2UiLCJleHAiOjE2MTAwMDAwMDB9.
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
- The header is base64 encoded JSON describing the algorithm.
- The payload contains claims such as
username
andexp
(expiration). - The signature ensures the token is not tampered with.
4. Storing JWT in Angular
Once received from the backend, the token can be stored for future API calls. Common storage options:
- LocalStorage – persists even after browser refresh.
- SessionStorage – persists until the browser tab is closed.
- Cookies – can be HTTP-only and secure.
Example: Storing Token
login(username: string, password: string) {
return this.http.post<{ token: string }>('/api/login', { username, password })
.pipe(
tap(response => localStorage.setItem('jwt', response.token))
);
}
5. Sending JWT in HTTP Requests
To access protected API endpoints, include the token in the Authorization
header using the Bearer scheme.
Example: Using HttpClient
const token = localStorage.getItem('jwt');
const headers = { Authorization: Bearer ${token}
};
this.http.get('/api/data', { headers })
.subscribe(data => console.log(data));
This ensures the server can verify the token before returning sensitive data.
6. Using HTTP Interceptors in Angular
For multiple API calls, manually adding headers is repetitive. Angular HTTP Interceptors automate this process.
auth.interceptor.ts
import { Injectable } from '@angular/core';
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = localStorage.getItem('jwt');
if (token) {
const cloned = req.clone({
headers: req.headers.set('Authorization', Bearer ${token}
)
});
return next.handle(cloned);
}
return next.handle(req);
}
}
app.module.ts
import { HTTP_INTERCEPTORS } from '@angular/common/http';
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
]
Now every request automatically includes the JWT.
7. Decoding JWT in Angular
Sometimes you need to decode a JWT to read claims like username or expiration. You can use jwt-decode library.
Installation
npm install jwt-decode
Usage
import jwt_decode from 'jwt-decode';
const token = localStorage.getItem('jwt');
if (token) {
const decoded: any = jwt_decode(token);
console.log(decoded.username);
console.log(new Date(decoded.exp * 1000));
}
8. Token Expiration Handling
JWTs have an expiration (exp
) claim. If a token expires, API requests fail with 401 Unauthorized.
Strategies to handle expiration:
- Automatic Logout – Log the user out when the token expires.
- Token Refresh – Request a new token using a refresh token.
Example: Checking Expiration
isTokenExpired(): boolean {
const token = localStorage.getItem('jwt');
if (!token) return true;
const decoded: any = jwt_decode(token);
return (decoded.exp * 1000) < Date.now();
}
9. Implementing a Login Flow
auth.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { tap } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class AuthService {
constructor(private http: HttpClient) {}
login(username: string, password: string) {
return this.http.post<{ token: string }>('/api/login', { username, password })
.pipe(
tap(response => localStorage.setItem('jwt', response.token))
);
}
logout() {
localStorage.removeItem('jwt');
}
getToken(): string | null {
return localStorage.getItem('jwt');
}
}
login.component.ts
login() {
this.authService.login(this.username, this.password).subscribe({
next: () => console.log('Login successful'),
error: err => console.error('Login failed', err)
});
}
10. Protecting Routes with Auth Guard
JWT authentication is often combined with CanActivate Guards to protect routes.
auth.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
constructor(private router: Router) {}
canActivate(): boolean {
const token = localStorage.getItem('jwt');
if (token) return true;
this.router.navigate(['/login']);
return false;
}
}
Applying Guard
{ path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuard] }
11. Refresh Tokens
To maintain long-term sessions without forcing frequent logins, you can use refresh tokens.
- Access token (JWT) – short-lived, used for API calls.
- Refresh token – long-lived, used to request new access tokens when expired.
Refresh Flow Example
refreshToken() {
const refreshToken = localStorage.getItem('refreshToken');
return this.http.post<{ token: string }>('/api/refresh', { refreshToken })
.pipe(
tap(response => localStorage.setItem('jwt', response.token))
);
}
12. Best Practices for JWT Authentication
- Use HTTPS – Always transmit JWTs over secure connections.
- Short-lived Access Tokens – Reduce exposure in case of compromise.
- Store Tokens Securely – Prefer HTTP-only cookies for sensitive apps.
- Validate Tokens Server-side – Never trust client-side validation alone.
- Implement Token Refresh – Maintain sessions without requiring frequent logins.
- Handle Expiration Gracefully – Prompt user to re-login or refresh token automatically.
13. Common JWT Security Concerns
- Token Theft – If an attacker gains access to the token, they can impersonate the user.
- Replay Attacks – Use short-lived tokens and refresh mechanisms.
- Cross-Site Scripting (XSS) – Avoid storing JWTs in localStorage in highly sensitive apps.
- Cross-Site Request Forgery (CSRF) – Use SameSite cookies or double submit pattern.
Leave a Reply