---
tags: [tech-patterns, auto-generated]
source: video-accessibility
created: 2026-04-30
---

# Azure SSO with MSAL: Browser-Based Token Exchange Pattern

## When to use
When implementing Single Page Application (SPA) authentication with Azure AD where the frontend handles OAuth token acquisition via PKCE flow, and the backend validates tokens without requiring client secrets. This pattern is appropriate when you need to avoid exposing credentials on the backend and want to leverage browser-native security features.

## Prerequisites
- Azure AD tenant with app registration configured as Single Page Application
- MSAL.js library (v2.0+) installed in frontend
- Backend HTTP endpoint for token validation (e.g., `POST /auth/microsoft`)
- Backend access to Microsoft's public JWKS endpoint
- Understanding of OAuth 2.0 PKCE flow

## Steps
1. Configure Azure AD app registration:
   - Set app type to "Single Page Application"
   - Add frontend URL to "Redirect URIs" (e.g., `http://localhost:3000`)
   - Enable implicit grant flow for ID tokens (not required, PKCE preferred)
   - Note the Client ID and Tenant ID

2. Configure MSAL in frontend (e.g., `src/msal-config.ts`):
   ```javascript
   const msalConfig = {
     auth: {
       clientId: process.env.REACT_APP_AZURE_CLIENT_ID,
       authority: `https://login.microsoftonline.com/${process.env.REACT_APP_AZURE_TENANT_ID}`,
       redirectUri: window.location.origin
     },
     cache: { cacheLocation: "sessionStorage" },
     system: { loggerOptions: { loggerCallback: console.log } }
   };
   ```

3. Trigger login popup in browser:
   ```javascript
   const loginPopup = async () => {
     const response = await publicClientApplication.loginPopup({
       scopes: ["openid", "profile"]
     });
     return response.idToken; // This is the ID token from Azure
   };
   ```

4. Send ID token to backend for validation:
   ```javascript
   const response = await fetch('/auth/microsoft', {
     method: 'POST',
     headers: { 'Content-Type': 'application/json' },
     body: JSON.stringify({ id_token: response.idToken }),
     credentials: 'include' // For httpOnly cookie
   });
   ```

5. Backend validates token signature against Microsoft's public JWKS:
   ```
   GET https://login.microsoftonline.com/{tenant-id}/discovery/v2.0/keys
   ```
   Verify:
   - Token signature matches public key from JWKS
   - `aud` (audience) claim matches your Client ID
   - `iss` (issuer) matches your tenant
   - Token not expired

6. Backend issues its own JWT (httpOnly, Secure, SameSite flags):
   ```
   Set-Cookie: jwt=...; HttpOnly; Secure; SameSite=Strict; Path=/
   ```

## Key Configuration
**Frontend `.env`:**
```
REACT_APP_AZURE_CLIENT_ID=your-client-id
REACT_APP_AZURE_TENANT_ID=your-tenant-id
```

**Backend token validation pseudocode:**
```
POST /auth/microsoft
  body: { id_token }
  
  1. Decode JWT header (no verification yet)
  2. Fetch JWKS from Microsoft
  3. Find public key matching kid from JWT header
  4. Verify signature with that public key
  5. Verify claims: aud, iss, exp, iat
  6. Create session / issue httpOnly JWT
  7. Return 200 with Set-Cookie header
```

## Gotchas
- **Do NOT make backend-to-backend calls to Microsoft token endpoint** – the browser already obtained the token via PKCE. A second call is redundant and suggests a configuration error (likely Azure app registered as "Web app" instead of "Single Page Application").
- **PKCE is handled automatically by MSAL.js** – no manual code_verifier/code_challenge needed in frontend.
- **Public JWKS endpoint requires no authentication** – you fetch it directly from Microsoft, no client secret needed for validation.
- **Token validation MUST happen on backend** – never trust token validation from frontend; always verify signature server-side.
- **Redirect URI must exactly match Azure registration** – including protocol, domain, port, and path.
- **httpOnly cookies prevent XSS token theft** – never store tokens in localStorage if possible.

## Source
Project: video-accessibility
