Authentication is one of the most essential aspects of modern web applications. In Angular, the most common way to authenticate users securely is through JSON Web Tokens (JWTs). JWT-based authentication provides a stateless and scalable way to handle user sessions between a frontend and backend API.
This post explains how to store JWT securely, attach it to HTTP requests, use interceptors, handle expiration, and implement logout—all with best practices and complete code examples.
1. Understanding JWT
A JSON Web Token (JWT) is a compact, URL-safe way to represent claims between two parties.
It is used to securely transmit information and verify user identity.
A typical JWT looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpZCI6IjEyMyIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTY5NjY0OTc0MywiZXhwIjoxNjk2NjUzMzQzfQ.
C5Kp1QyP8wVq3wPfXc1yqG2IbFzGlUTlRzG49eXQabE
A JWT has three parts separated by dots (.
):
- Header – contains the algorithm and token type.
- Payload – contains user data and claims (e.g., userId, role, exp).
- Signature – ensures integrity and verifies authenticity.
2. Why Use JWT in Angular
JWT-based authentication is stateless, meaning the server does not store session information.
The token itself contains all necessary data, verified using a secret key.
Advantages:
- Stateless — no server-side session storage.
- Easy to scale for distributed systems.
- Works seamlessly with RESTful APIs.
- Can store roles and permissions in payload.
- Lightweight and fast to verify.
3. Receiving JWT from API
When a user logs in, the backend verifies credentials and returns a JWT in the response.
Example Response:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5c..."
}
Example Angular login method:
login() {
this.http.post('https://api.example.com/login', this.credentials)
.subscribe((res: any) => {
localStorage.setItem('token', res.token);
});
}
The token must be securely stored so it can be used in future API requests.
4. Storing JWT Securely
Angular provides two common browser storage options:
- localStorage
- sessionStorage
Using localStorage
Persists even after the browser is closed.
localStorage.setItem('token', res.token);
Retrieve it later:
const token = localStorage.getItem('token');
Using sessionStorage
Cleared when the tab or browser is closed.
sessionStorage.setItem('token', res.token);
Retrieve it later:
const token = sessionStorage.getItem('token');
5. Comparing Storage Methods
Storage Type | Persists After Tab Close | Protected from XSS | Recommended Use |
---|---|---|---|
localStorage | Yes | No | Long sessions |
sessionStorage | No | No | Short sessions |
In-memory | No | Yes | Temporary tokens |
If security is the top priority, in-memory storage is ideal because tokens disappear when the page reloads.
For most apps, localStorage
or sessionStorage
is used depending on session needs.
6. Attaching JWT to API Requests
Once stored, the JWT must be included in all requests to protected API endpoints.
Manual Example:
const token = localStorage.getItem('token');
const headers = {
Authorization: Bearer ${token}
};
this.http.get('https://api.example.com/profile', { headers })
.subscribe(res => console.log(res));
This attaches the JWT in the HTTP Authorization
header, following the Bearer Token convention.
7. Automating Token Attachment with HTTP Interceptors
Manually adding the token to every request is repetitive and error-prone.
Angular’s HTTP Interceptors provide a clean way to handle this globally.
Step 1: Create JWT Interceptor
import { Injectable } from '@angular/core';
import {
HttpInterceptor,
HttpRequest,
HttpHandler,
HttpEvent
} from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable()
export class JwtInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = localStorage.getItem('token');
if (token) {
const cloned = req.clone({
setHeaders: { Authorization: Bearer ${token}
}
});
return next.handle(cloned);
}
return next.handle(req);
}
}
Step 2: Register Interceptor
In your AppModule
:
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { JwtInterceptor } from './jwt.interceptor';
@NgModule({
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true }
]
})
export class AppModule {}
Now all outgoing HTTP requests will automatically include the token.
8. Creating AuthService for JWT Management
You can centralize token handling in a service:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import jwtDecode from 'jwt-decode';
@Injectable({ providedIn: 'root' })
export class AuthService {
constructor(private http: HttpClient) {}
login(credentials: any) {
return this.http.post('https://api.example.com/login', credentials)
.subscribe((res: any) => {
localStorage.setItem('token', res.token);
});
}
getToken(): string | null {
return localStorage.getItem('token');
}
getDecodedToken(): any {
const token = this.getToken();
return token ? jwtDecode(token) : null;
}
isAuthenticated(): boolean {
return !!this.getToken();
}
logout(): void {
localStorage.removeItem('token');
}
}
This service handles login, token storage, decoding, and logout in one place.
9. Checking Token Expiration
Tokens usually contain an exp field that specifies expiration in seconds.
Example JWT payload:
{
"userId": "123",
"role": "admin",
"exp": 1723478843
}
Token expiration check:
import jwtDecode from 'jwt-decode';
checkTokenExpiration(): boolean {
const token = localStorage.getItem('token');
if (!token) return false;
const decoded: any = jwtDecode(token);
const expiryTime = decoded.exp * 1000;
return Date.now() < expiryTime;
}
If expired, automatically logout the user:
if (!this.checkTokenExpiration()) {
this.logout();
this.router.navigate(['/login']);
}
10. Protecting Routes Using Guards
JWTs can be used with Angular route guards to restrict access to certain routes.
AuthGuard Example:
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { AuthService } from './auth.service';
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
constructor(private auth: AuthService, private router: Router) {}
canActivate(): boolean {
if (this.auth.isAuthenticated()) {
return true;
} else {
this.router.navigate(['/login']);
return false;
}
}
}
Apply it in your routes:
const routes: Routes = [
{ path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuard] }
];
11. Decoding JWT to Access User Info
You can decode JWTs to extract user details like role or ID:
import jwtDecode from 'jwt-decode';
const token = localStorage.getItem('token');
const payload: any = jwtDecode(token!);
console.log(payload.userId);
console.log(payload.role);
This helps in role-based access control or UI rendering.
12. Logging Out
Logging out is simple: remove the token and redirect to login.
logout() {
localStorage.removeItem('token');
this.router.navigate(['/login']);
}
This ensures the user session is terminated client-side.
13. Handling Token Refresh
For longer sessions, implement refresh tokens.
The server issues a short-lived JWT and a long-lived refresh token.
refreshToken() {
return this.http.post('/api/refresh', {}).subscribe((res: any) => {
localStorage.setItem('token', res.token);
});
}
14. Common JWT Errors and Fixes
Error | Cause | Solution |
---|---|---|
401 Unauthorized | Missing or expired token | Add valid token in header |
Token not found | Storage cleared | Redirect to login |
Signature invalid | Tampered token | Verify signing secret on backend |
Token expired | Time exceeded | Use refresh token or logout |
15. Best Practices for JWT in Angular
- Use HTTPS — Never send tokens over HTTP.
- Avoid sensitive data inside JWT payload.
- Use short expiration times.
- Use refresh tokens for long sessions.
- Clear tokens on logout or inactivity.
- Validate tokens on the backend for every request.
- Handle errors gracefully (e.g., redirect on 401).
16. Example Full Authentication Flow
@Component({
selector: 'app-login',
templateUrl: './login.component.html'
})
export class LoginComponent {
credentials = { email: '', password: '' };
constructor(private auth: AuthService, private router: Router) {}
login() {
this.auth.login(this.credentials);
}
}
auth.service.ts
@Injectable({ providedIn: 'root' })
export class AuthService {
constructor(private http: HttpClient, private router: Router) {}
login(credentials: any) {
this.http.post('/api/login', credentials).subscribe((res: any) => {
localStorage.setItem('token', res.token);
this.router.navigate(['/dashboard']);
});
}
logout() {
localStorage.removeItem('token');
this.router.navigate(['/login']);
}
}
jwt.interceptor.ts
@Injectable()
export class JwtInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler) {
const token = localStorage.getItem('token');
const cloned = token
? req.clone({ setHeaders: { Authorization: Bearer ${token}
} })
: req;
return next.handle(cloned);
}
}
17. Example Protected Component
@Component({
selector: 'app-dashboard',
template: `
<h2>Welcome, {{ user?.email }}</h2>
<button (click)="logout()">Logout</button>
`
})
export class DashboardComponent implements OnInit {
user: any;
constructor(private auth: AuthService) {}
ngOnInit() {
const decoded = this.auth.getDecodedToken();
this.user = decoded;
}
logout() {
this.auth.logout();
}
}
18. Validating JWT on Backend
Always verify the token on the server:
const jwt = require('jsonwebtoken');
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) return res.sendStatus(401);
jwt.verify(token, 'SECRET_KEY', (err, user) => {
if (err) return res.sendStatus(403);
req.user = user;
next();
});
}
This ensures the token was signed by your server and not tampered with.
19. Handling Token Errors Globally
You can handle 401
responses in an HTTP interceptor:
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
constructor(private auth: AuthService) {}
intercept(req: HttpRequest<any>, next: HttpHandler) {
return next.handle(req).pipe(
catchError(err => {
if (err.status === 401) {
this.auth.logout();
}
return throwError(() => err);
})
);
}
}
This ensures users are logged out automatically if their token expires.
Leave a Reply