DEV Community

Cover image for How to Set Up JWT Authentication in Angular Micro-Frontend Apps
mhmoud ashour
mhmoud ashour

Posted on

How to Set Up JWT Authentication in Angular Micro-Frontend Apps

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)
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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]))
  ]
};
Enter fullscreen mode Exit fullscreen mode

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']);
};
Enter fullscreen mode Exit fullscreen mode

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'
    })
  }
];
Enter fullscreen mode Exit fullscreen mode

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);
    })
  );
};
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)