Module 20: Security & Auth Across Boundaries
Chapter 20 • Advanced
Security & Auth Across Boundaries
The Real Problem
Scenario: User authenticates in Host. User navigates to Products. Products doesn't know user is authenticated. User sees "Login" button. User is confused.
The Question: How do I ensure a user authenticated in Host is authenticated in Products and Checkout, without creating security holes?
This is enterprise gold.
The Production Reality
Security Challenges
- Auth Ownership - Who owns authentication? Host? Remotes?
- Token Sharing - How to share tokens safely?
- CSP and Module Federation - What breaks?
- XSS Prevention - Products XSS shouldn't affect Checkout
- Role-Based Access - Different apps, different permissions
The Question: How do we solve each challenge securely?
Strategy 1: Auth Ownership (Host Owns Auth)
The Pattern
Host authenticates user. Host shares auth state with remotes. Remotes trust Host.
Host App: Authentication
// host-app/src/app/auth/auth.service.ts
@Injectable({ providedIn: 'root' })
export class AuthService {
private user$ = new BehaviorSubject<User | null>(null)
private token: string | null = null
constructor(private http: HttpClient) {
this.loadAuthState()
}
login(credentials: LoginCredentials): Observable<User> {
return this.http.post<AuthResponse>('/api/auth/login', credentials)
.pipe(
tap(response => {
this.token = response.token
this.user$.next(response.user)
this.saveAuthState(response)
})
)
}
getToken(): string | null {
return this.token
}
getUser(): Observable<User | null> {
return this.user$.asObservable()
}
isAuthenticated(): boolean {
return this.token !== null
}
private saveAuthState(auth: AuthResponse) {
// Use httpOnly cookie (secure) or localStorage (less secure)
localStorage.setItem('auth_token', auth.token)
localStorage.setItem('user', JSON.stringify(auth.user))
}
private loadAuthState() {
const token = localStorage.getItem('auth_token')
const userStr = localStorage.getItem('user')
if (token && userStr) {
this.token = token
this.user$.next(JSON.parse(userStr))
}
}
}
Sharing Auth with Remotes
// Host App - Share auth via event bus
@Injectable({ providedIn: 'root' })
export class AuthSharingService {
constructor(
private auth: AuthService,
private eventBus: EventBus
) {
// Share auth state when it changes
this.auth.getUser().subscribe(user => {
if (user) {
this.eventBus.emit({
event: 'auth:user-authenticated',
version: '1.0.0',
data: {
user: {
id: user.id,
email: user.email,
roles: user.roles
},
token: this.auth.getToken() // ⚠️ Security consideration below
}
})
} else {
this.eventBus.emit({
event: 'auth:user-logged-out',
version: '1.0.0',
data: {}
})
}
})
}
}
Products App: Receive Auth
// products-app/src/app/auth/auth.service.ts
@Injectable({ providedIn: 'root' })
export class ProductsAuthService {
private user$ = new BehaviorSubject<User | null>(null)
constructor(private eventBus: EventBus) {
// Listen for auth events from Host
this.eventBus.on('auth:user-authenticated').subscribe(event => {
if (event.version === '1.0.0') {
this.user$.next(event.data.user)
// Store token for API calls
if (event.data.token) {
this.setToken(event.data.token)
}
}
})
this.eventBus.on('auth:user-logged-out').subscribe(() => {
this.user$.next(null)
this.clearToken()
})
}
getUser(): Observable<User | null> {
return this.user$.asObservable()
}
isAuthenticated(): boolean {
return this.user$.value !== null
}
private setToken(token: string) {
// Store token for API calls
localStorage.setItem('auth_token', token)
}
private clearToken() {
localStorage.removeItem('auth_token')
}
getToken(): string | null {
return localStorage.getItem('auth_token')
}
}
Strategy 2: Token Sharing Safely
The Problem
Sharing tokens via events is risky. Tokens can be intercepted.
Option 1: httpOnly Cookies (Most Secure)
// Backend sets httpOnly cookie
app.post('/api/auth/login', (req, res) => {
const token = generateToken(req.body)
// Set httpOnly cookie (not accessible via JavaScript)
res.cookie('auth_token', token, {
httpOnly: true, // Not accessible via document.cookie
secure: true, // HTTPS only
sameSite: 'strict' // CSRF protection
})
res.json({ user: req.user })
})
Why This Works:
- ✅ Token not accessible via JavaScript (XSS protection)
- ✅ Automatically sent with requests
- ✅ Same origin only (CORS protection)
Limitation:
- ❌ Remotes on different origins can't access
Option 2: localStorage with Validation (Less Secure)
// Host validates token before sharing
@Injectable({ providedIn: 'root' })
export class AuthSharingService {
shareAuth(user: User) {
// Validate token is still valid
this.http.get('/api/auth/validate').subscribe({
next: () => {
// Token valid, share user info (not token)
this.eventBus.emit({
event: 'auth:user-authenticated',
version: '1.0.0',
data: {
user: {
id: user.id,
email: user.email,
roles: user.roles
}
// Don't share token - remotes get from localStorage
}
})
},
error: () => {
// Token invalid, logout
this.auth.logout()
}
})
}
}
Why This Works:
- ✅ Token stored in localStorage (accessible to remotes)
- ✅ Validation before sharing
- ✅ Remotes can access token for API calls
Risk:
- ⚠️ Token accessible via JavaScript (XSS risk)
Strategy 3: CSP and Module Federation
The Problem
Content Security Policy (CSP) can block Module Federation.
The Solution: Configure CSP for Module Federation
<!-- index.html -->
<meta http-equiv="Content-Security-Policy"
content="
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://products.example.com https://checkout.example.com;
style-src 'self' 'unsafe-inline';
connect-src 'self' https://api.example.com;
frame-src 'self';
">
Required Directives:
script-src- Allow remote scriptsconnect-src- Allow API callsframe-src- Allow iframes (if used)
Security Trade-off:
- ⚠️
unsafe-inlineandunsafe-evalrequired for Module Federation - ⚠️ Less strict CSP
- ✅ Still better than no CSP
Strategy 4: Preventing XSS Escalation
The Problem
Products app has XSS vulnerability. Can it affect Checkout?
The Solution: Isolate Apps
// Host App - Sanitize data from remotes
@Injectable({ providedIn: 'root' })
export class SanitizationService {
sanitizeHtml(html: string): string {
// Use DOMPurify or similar
return DOMPurify.sanitize(html)
}
sanitizeUrl(url: string): string {
// Validate URL
try {
const parsed = new URL(url)
if (['http:', 'https:'].includes(parsed.protocol)) {
return url
}
} catch {
return ''
}
return ''
}
}
Angular Sanitization
// Products App - Use Angular sanitization
@Component({
template: `
<div [innerHTML]="sanitizedContent"></div>
`
})
export class ProductsComponent {
sanitizedContent: SafeHtml
constructor(private sanitizer: DomSanitizer) {}
ngOnInit() {
// Sanitize user content
this.sanitizedContent = this.sanitizer.sanitize(
SecurityContext.HTML,
this.product.description
)
}
}
Key: Never trust data from remotes. Always sanitize.
Strategy 5: Role-Based Access Across Apps
The Problem
User has "admin" role. Should they see admin features in Products? Checkout?
The Solution: Share Roles, Enforce Locally
// Host App - Share user roles
@Injectable({ providedIn: 'root' })
export class AuthSharingService {
shareAuth(user: User) {
this.eventBus.emit({
event: 'auth:user-authenticated',
version: '1.0.0',
data: {
user: {
id: user.id,
email: user.email,
roles: user.roles // Share roles
}
}
})
}
}
// Products App - Check roles
@Injectable({ providedIn: 'root' })
export class ProductsAuthService {
private user$ = new BehaviorSubject<User | null>(null)
hasRole(role: string): boolean {
const user = this.user$.value
return user?.roles?.includes(role) ?? false
}
hasAnyRole(roles: string[]): boolean {
const user = this.user$.value
return roles.some(role => user?.roles?.includes(role) ?? false)
}
}
// Products Component - Use role check
@Component({
template: `
<button *ngIf="canEdit()" (click)="edit()">Edit</button>
`
})
export class ProductsComponent {
constructor(private auth: ProductsAuthService) {}
canEdit(): boolean {
return this.auth.hasRole('admin') || this.auth.hasRole('editor')
}
}
Route Guards with Roles
// Products App - Route guard
@Injectable({ providedIn: 'root' })
export class RoleGuard implements CanActivate {
constructor(private auth: ProductsAuthService) {}
canActivate(route: ActivatedRouteSnapshot): boolean {
const requiredRoles = route.data['roles'] as string[]
if (!requiredRoles) {
return true
}
return this.auth.hasAnyRole(requiredRoles)
}
}
// Products routes
const routes: Routes = [
{
path: 'admin',
component: AdminComponent,
canActivate: [RoleGuard],
data: { roles: ['admin'] }
}
]
Token Expiration Handling
The Problem
Token expires mid-session. What happens?
The Solution: Token Refresh
// Host App - Token refresh
@Injectable({ providedIn: 'root' })
export class AuthService {
private refreshToken$ = new BehaviorSubject<string | null>(null)
constructor(private http: HttpClient) {
this.startTokenRefresh()
}
private startTokenRefresh() {
// Refresh token before expiration
interval(5 * 60 * 1000) // Every 5 minutes
.pipe(
switchMap(() => this.refreshToken())
)
.subscribe({
next: (response) => {
this.token = response.token
this.saveAuthState(response)
this.shareAuth(response.user)
},
error: () => {
// Refresh failed, logout
this.logout()
}
})
}
private refreshToken(): Observable<AuthResponse> {
const refreshToken = localStorage.getItem('refresh_token')
return this.http.post<AuthResponse>('/api/auth/refresh', {
refreshToken
})
}
}
Production Checklist
- [ ] Auth ownership defined (Host owns auth)
- [ ] Token sharing strategy chosen (httpOnly cookies or localStorage)
- [ ] CSP configured for Module Federation
- [ ] XSS prevention (sanitization)
- [ ] Role-based access implemented
- [ ] Token expiration handling
- [ ] Secure communication (HTTPS)
- [ ] CORS configured correctly
Key Takeaways
- Host owns auth - Single source of truth
- Share safely - httpOnly cookies preferred
- Sanitize everything - Never trust remotes
- Enforce locally - Each app checks permissions
- Handle expiration - Refresh tokens automatically
Remember: In micro frontends, security is architecture, not middleware.
Congratulations! You've completed the Production Architecture phase. You now understand how to build, test, and secure micro frontends at scale.