---
title: "Azure AD / MSAL Authentication"
aliases: [azure-ad, msal, sso, pkce]
tags: [azure-ad, msal, auth, sso, pkce, security]
sources:
  - "01 Projects/gmal-scope-builder"
  - "01 Projects/enterprise-ai-hub-nexus"
  - "01 Projects/cinema-studio-pro"
  - "01 Projects/modcomms"
  - "01 Projects/baic_dashboard"
  - "01 Projects/sandbox-notebookllamalm-nextjs"
  - "daily/2026-04-15.md"
created: 2026-04-15
updated: 2026-04-15
---

# Azure AD / MSAL Authentication

Oliver Agency standard SSO. MSAL handles token acquisition; backend validates via Azure AD JWT.

## Key Takeaways
- All Oliver-internal tools requiring login use Azure AD (MSAL)
- MSAL v5 introduced breaking changes — `clearCache()` for logout, NOT `logout()`
- Send **ID token** to backend, NOT the Graph access token
- Use `DEV_AUTH_BYPASS=true` env var for local dev to skip auth
- Backend validates via `JWTValidator.php` (PHP) or Azure AD middleware (Python)
- PKCE flow is used for SPAs (Enterprise Nexus); standard OAuth2 for server-side

## When to Use
Any Oliver internal tool that needs to know who the user is. Skip for pure tools/utilities.

## Key Details

### Frontend Config
```js
// authConfig.js / authConfig.ts
{
  auth: {
    clientId: process.env.AZURE_CLIENT_ID,
    authority: `https://login.microsoftonline.com/${TENANT_ID}`,
    redirectUri: window.location.origin
  },
  cache: { cacheLocation: "sessionStorage" }
}
```

### Required Env Vars
```env
AZURE_TENANT_ID=
AZURE_CLIENT_ID=
AZURE_CLIENT_SECRET=   # server-side only
DEV_AUTH_BYPASS=true   # local dev only
```

### Scopes
- **M365 integration** requires delegated scopes + `offline_access` (Enterprise Nexus)
- **Graph API** access requires explicit scope grants in Azure App Registration

### MSAL v5 Breaking Changes
```js
// Old (MSAL v4):
instance.logout()
// New (MSAL v5):
instance.clearCache()
```

### Token Validation (Backend)
- **PHP:** `JWTValidator.php` + `AuthMiddleware.php` (Cinema Studio Pro)
- **Python:** Azure AD middleware checks Bearer token against tenant

## Projects Using This Pattern
- [[01 Projects/gmal-scope-builder/GMAL Scope Builder|GMAL Scope Builder]] — MSAL v5, DEV_AUTH_BYPASS, Azure SSO
- [[01 Projects/enterprise-ai-hub-nexus/Enterprise AI Hub Nexus|Enterprise Nexus]] — PKCE, delegated scopes, offline_access, M365
- [[01 Projects/cinema-studio-pro/Cinema Studio Pro|Cinema Studio Pro]] — MSAL + JWTValidator.php backend
- [[01 Projects/modcomms/Mod Comms|Mod Comms]] — Azure AD, DISABLE_AUTH for local dev
- [[01 Projects/baic_dashboard/BAIC Dashboard|BAIC Dashboard]] — MSAL, Azure App Registration required
- [[01 Projects/sandbox-notebookllamalm-nextjs/Sandbox NotebookLM|Sandbox NotebookLM]] — Microsoft SSO + signup/login

## Gotchas & Lessons
- MSAL v5 `clearCache()` not `logout()` — fix was needed in GMAL (2026-03-30)
- Send ID token to backend, not Graph access token — GMAL bug fix (2026-03-30)
- `needs_reconsent` flag: if Graph token refresh fails, prompt user to re-login (Enterprise Nexus)
- Azure App Registration must have the delegated scopes explicitly added for M365 features
- `offline_access` scope required for refresh token to work (Enterprise Nexus M365)
- Proactive token refresh needed for long-running Celery jobs (Enterprise Nexus SharePoint sync)
- **SPA platform type is required for PKCE** — Azure Portal → Authentication → Platform configurations must be set to "Single-page application", NOT "Web". "Web" expects a client secret; PKCE SPAs don't have one → silent auth failure (2026-04-15)
- **OAuth state mismatch** after server restart: session cookie from previous attempt doesn't match new state — always ask user to try different browser before deep-diving Azure config (2026-04-15)

## Related
- [[wiki/tech-patterns/fastapi-python-docker|fastapi-python-docker]] — backend receiving tokens
- [[wiki/tech-patterns/react-vite-typescript|react-vite-typescript]] — frontend MSAL integration
- [[wiki/tech-patterns/nextjs-fastapi-fullstack|nextjs-fastapi-fullstack]] — Next.js PKCE flow
