If you’ve ever tried to implement JWT authentication in an Angular Micro-Frontend architecture, you know the pain.
It works fine in a single app. But the moment you split into multiple micro-apps, questions start piling up:
- Where does the token live?
- How does each micro-app access it?
- How do you protect routes across different apps?
- What happens when the token expires?
I’ve solved this in real enterprise projects. Here’s exactly how I do it.
The Problem With JWT in Micro-Frontends
In a standard Angular app, JWT auth is straightforward — store the token, attach it to requests, guard your routes.
But in a Micro-Frontend setup you have multiple independent apps:
Shell App (Port 4200)
├── Auth App (Port 4201)
└── Dashboard App (Port 4202)
Each app is built and deployed separately. So how do they share authentication state?
The Solution — Centralized Auth in the Shell
The key principle is simple:
Handle authentication once in the Shell app. Let all micro-apps inherit it.
Here’s how:
Step 1 — Store the Token Centrally
In your Shell app, create an Auth Service:
// shell/src/app/services/auth.service.ts
import { Injectable, signal } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class AuthService {
private tokenKey = 'auth_token';
isAuthenticated = signal<boolean>(this.hasValidToken());
login(token: string): void {
localStorage.setItem(this.tokenKey, token);
this.isAuthenticated.set(true);
}
logout(): void {
localStorage.removeItem(this.tokenKey);
this.isAuthenticated.set(false);
}
getToken(): string | null {
return localStorage.getItem(this.tokenKey);
}
private hasValidToken(): boolean {
const token = localStorage.getItem(this.tokenKey);
return !!token;
}
}
Step 2 — HTTP Interceptor for Automatic Token Attachment
Instead of manually adding the token to every request, use an interceptor:
// shell/src/app/interceptors/auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from '../services/auth.service';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
const token = authService.getToken();
if (token) {
const authReq = req.clone({
headers: req.headers.set('Authorization', `Bearer ${token}`)
});
return next(authReq);
}
return next(req);
};
Register it in your app config:
// shell/src/app/app.config.ts
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './interceptors/auth.interceptor';
export const appConfig = {
providers: [
provideHttpClient(withInterceptors([authInterceptor]))
]
};
Step 3 — Route Guards
Protect your routes with guards:
// shell/src/app/guards/auth.guard.ts
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from '../services/auth.service';
export const authGuard = () => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isAuthenticated()) {
return true;
}
return router.createUrlTree(['/auth/login']);
};
export const guestGuard = () => {
const authService = inject(AuthService);
const router = inject(Router);
if (!authService.isAuthenticated()) {
return true;
}
return router.createUrlTree(['/dashboard']);
};
Apply them to your routes:
// shell/src/app/app.routes.ts
import { Routes } from '@angular/router';
import { authGuard, guestGuard } from './guards/auth.guard';
export const routes: Routes = [
{
path: 'auth',
canActivate: [guestGuard],
loadRemoteModule({
remoteEntry: 'http://localhost:4201/remoteEntry.js',
remoteName: 'auth',
exposedModule: './AuthModule'
})
},
{
path: 'dashboard',
canActivate: [authGuard],
loadRemoteModule({
remoteEntry: 'http://localhost:4202/remoteEntry.js',
remoteName: 'dashboard',
exposedModule: './DashboardModule'
})
}
];
Step 4 — Handle Token Expiry
Add token expiry handling to your interceptor:
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { catchError, throwError } from 'rxjs';
import { AuthService } from '../services/auth.service';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
const router = inject(Router);
const token = authService.getToken();
const authReq = token
? req.clone({ headers: req.headers.set('Authorization', `Bearer ${token}`) })
: req;
return next(authReq).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
authService.logout();
router.navigate(['/auth/login']);
}
return throwError(() => error);
})
);
};
How Micro-Apps Access Auth State
Since the Shell handles auth centrally, micro-apps simply read from localStorage:
// dashboard/src/app/services/token.service.ts
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class TokenService {
getToken(): string | null {
return localStorage.getItem('auth_token');
}
isLoggedIn(): boolean {
return !!this.getToken();
}
}
No need to duplicate auth logic in every micro-app. ✅
The Complete Flow
User visits /dashboard
→ authGuard checks isAuthenticated()
→ Not authenticated → redirect to /auth/login
→ User logs in → token stored in localStorage
→ Shell updates isAuthenticated signal
→ Redirect to /dashboard
→ Dashboard micro-app loads
→ HTTP requests automatically include Bearer token
→ If 401 received → logout and redirect to login
Want This Already Set Up?
I built NgMFE Starter Kit — a complete Angular 21 Micro-Frontend boilerplate with all of this pre-configured:
- ⚡ Angular 21 + Native Federation
- 🔐 JWT Auth + HTTP Interceptor + Route Guards
- 🌍 Arabic RTL support
- 🌙 Dark/Light mode
- 🚀 CI/CD with GitHub Actions + Vercel
- ✅ 13 unit tests passing
🔴 Live Demo: https://ng-mfe-shell.vercel.app
(login: admin/admin)
🛒 Get the kit directly:
👉 https://mhmoudashour.gumroad.com/l/hdditr
💼 Need it set up for your project?
👉 https://www.upwork.com/services/product/development-it-set-up-angular-micro-frontend-architecture-for-your-enterprise-app-2037100004401414520?ref=project_share
Building Micro-Frontend auth and hit a problem? Drop it in the comments — happy to help! 🙏
Top comments (0)