20

Module 20: Security & Auth Across Boundaries

Chapter 20 • Advanced

50 min

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

  1. Auth Ownership - Who owns authentication? Host? Remotes?
  2. Token Sharing - How to share tokens safely?
  3. CSP and Module Federation - What breaks?
  4. XSS Prevention - Products XSS shouldn't affect Checkout
  5. 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

typescript.js
// 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

typescript.js
// 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

typescript.js
// 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)

typescript.js
// 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)

typescript.js
// 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

html.js
<!-- 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 scripts
  • connect-src - Allow API calls
  • frame-src - Allow iframes (if used)

Security Trade-off:

  • ⚠️ unsafe-inline and unsafe-eval required 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

typescript.js
// 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

typescript.js
// 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

typescript.js
// 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
        }
      }
    })
  }
}
typescript.js
// 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)
  }
}
typescript.js
// 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

typescript.js
// 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)
  }
}
typescript.js
// 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

typescript.js
// 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

  1. Host owns auth - Single source of truth
  2. Share safely - httpOnly cookies preferred
  3. Sanitize everything - Never trust remotes
  4. Enforce locally - Each app checks permissions
  5. 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.