Azure Auth - Microsoft Entra ID for React + Cloudflare Workers
Package Versions: @azure/msal-react@5.0.2, @azure/msal-browser@5.0.2, jose@6.1.3
Breaking Changes: MSAL v4→v5 migration (January 2026), Azure AD B2C sunset (May 2025 - new signups blocked, existing until 2030), ADAL retirement (Sept 2025 - complete)
Last Updated: 2026-01-21
Architecture Overview
CODEBLOCK0
Key Constraint: MSAL.js does NOT work in Cloudflare Workers (relies on browser/Node.js APIs). Use jose library for backend token validation.
Quick Start
1. Install Dependencies
CODEBLOCK1
2. Azure Portal Setup
- 1. Go to Microsoft Entra ID → App registrations → New registration
- Set Redirect URI to
http://localhost:5173 (SPA type) - Note the Application (client) ID and Directory (tenant) ID
- Under Authentication:
- Enable
Access tokens and
ID tokens
- Add production redirect URI
- 5. Under API permissions:
- Add
User.Read (Microsoft Graph)
- Grant admin consent if required
Frontend: MSAL React Setup
Configuration (src/auth/msal-config.ts)
CODEBLOCK2
MsalProvider Setup (src/main.tsx)
CODEBLOCK3
Protected Route Component
CODEBLOCK4
Acquiring Tokens for API Calls
CODEBLOCK5
Backend: Cloudflare Workers JWT Validation
Why jose Instead of MSAL
MSAL.js relies on browser APIs (localStorage, sessionStorage) and Node.js crypto modules that don't exist in Cloudflare Workers' V8 isolate runtime. The jose library is pure JavaScript and works perfectly in Workers.
JWT Validation (src/auth/validate-token.ts)
CODEBLOCK6
Worker Middleware Pattern
CODEBLOCK7
Common Errors & Fixes
1. AADSTS50058 - Silent Sign-In Loop
Error: "A silent sign-in request was sent but no user is signed in"
Cause: acquireTokenSilent called when no cached user exists.
Fix:
CODEBLOCK8
2. AADSTS700084 - Refresh Token Expired
Error: "The refresh token was issued to a single page app (SPA), and therefore has a fixed, limited lifetime of 1.00:00:00"
Cause: SPA refresh tokens expire after 24 hours. Cannot be extended.
Fix:
CODEBLOCK9
3. React Router v6 Redirect Loop
Error: Infinite redirects between login page and app.
Cause: React Router v6 may strip the hash fragment containing auth response.
Fix: Use custom NavigationClient:
CODEBLOCK10
4. NextJS Dynamic Route Error
Error: no_cached_authority_error in dynamic routes.
Cause: MSAL instance not properly initialized before component renders.
Fix: Initialize MSAL in _app.tsx before any routing:
CODEBLOCK11
5. Safari/Edge Cookie Issues
Error: Auth state lost, infinite loop on Safari or Edge. On iOS 18 Safari specifically, silent token refresh fails with AADSTS50058 even when third-party cookies are enabled.
Source: GitHub Issue #7384
Cause: These browsers have stricter cookie policies affecting session storage. iOS 18 Safari doesn't store the required session cookies for login.microsoftonline.com, even with third-party cookies explicitly allowed in settings.
Testing Note: Works in Chrome on iOS 18, but fails in Safari on iOS 18.
Fix: Enable cookie storage in MSAL config:
CODEBLOCK12
iOS 18 Safari Limitation: If users still experience issues on iOS 18 Safari after enabling cookie storage, this is a known browser limitation with no current workaround. Recommend using Chrome on iOS or desktop browser.
6. JWKS URL Not Found (Workers)
Error: Failed to fetch JWKS from .well-known/jwks.json.
Cause: Azure AD doesn't serve JWKS at the standard OpenID Connect path.
Fix: Fetch openid-configuration first, then use jwks_uri:
CODEBLOCK13
7. React Router Loader State Conflict
Error: React warning about updating state during render when using acquireTokenSilent in React Router loaders.
Source: GitHub Issue #7068
Cause: Using the same PublicClientApplication instance in both the router loader and MsalProvider causes state updates during rendering.
Fix: Call initialize() again in the loader:
CODEBLOCK14
8. setActiveAccount Doesn't Trigger Re-render (Community-sourced)
Error: Components using useMsal() don't update after calling setActiveAccount().
Source: GitHub Issue #6989
Verified: Multiple users confirmed in GitHub issue
Cause: setActiveAccount() updates the MSAL instance but doesn't notify React of the change.
Fix: Force re-render with state:
const [accountKey, setAccountKey] = useState(0);
const switchAccount = (newAccount) => {
msalInstance.setActiveAccount(newAccount);
setAccountKey(prev => prev + 1); // Force update
};
Multi-Tenant vs Single-Tenant
Single Tenant (Recommended for Enterprise)
CODEBLOCK16
- - Only users from your organization can sign in
- Token issuer: INLINECODE15
Multi-Tenant
CODEBLOCK17
- - Users from any Azure AD tenant can sign in
- Token issuer varies by user's tenant
- Backend validation must handle multiple issuers:
CODEBLOCK18
Environment Variables
Frontend (.env)
CODEBLOCK19
Backend (wrangler.jsonc)
CODEBLOCK20
Azure AD B2C Sunset
Timeline:
- - May 1, 2025: Azure AD B2C no longer available for new customer signups
- March 15, 2026: Azure AD B2C P2 discontinued for all customers
- May 2030: Microsoft will continue supporting existing B2C customers with standard support
Source: Microsoft Q&A
Existing B2C Customers: Can continue using B2C until 2030, but should plan migration to Entra External ID.
New Projects: Use Microsoft Entra External ID for consumer/customer identity scenarios.
Migration Status: As of January 2026, automated migration tools are in testing phase. Manual migration guidance available at Microsoft Learn.
Migration Path:
- - Different authority URL format (
{tenant}.ciamlogin.com vs {tenant}.b2clogin.com) - Updated SDK support (same MSAL libraries)
- New pricing model (consumption-based)
- Self-Service Password Reset (SSPR) approach available for user migration
- Seamless migration samples on GitHub (preview)
See: https://learn.microsoft.com/en-us/entra/external-id/
Migration Guide: https://learn.microsoft.com/en-us/entra/external-id/customers/how-to-migrate-users
ADAL Retirement (Complete)
Status: Azure AD Authentication Library (ADAL) was retired on September 30, 2025. Apps using ADAL no longer receive security updates.
If you're migrating from ADAL:
- 1. ADAL → MSAL migration is required
- ADAL used v1.0 endpoints; MSAL uses v2.0 endpoints
- Token cache format differs - users must re-authenticate
- Scopes replace "resources" in token requests
Key Migration Changes:
CODEBLOCK21
See: https://learn.microsoft.com/en-us/entra/msal/javascript/migration/msal-net-migration
Resources
Azure Auth - 用于 React + Cloudflare Workers 的 Microsoft Entra ID
包版本: @azure/msal-react@5.0.2, @azure/msal-browser@5.0.2, jose@6.1.3
重大变更: MSAL v4→v5 迁移(2026年1月),Azure AD B2C 停用(2025年5月 - 新注册已阻止,现有用户可使用至2030年),ADAL 退役(2025年9月 - 已完成)
最后更新: 2026-01-21
架构概览
┌─────────────────────┐ ┌──────────────────────┐ ┌─────────────────────┐
│ React SPA │────▶│ Microsoft Entra ID │────▶│ Cloudflare Worker │
│ @azure/msal-react │ │ (login.microsoft) │ │ jose JWT 验证 │
└─────────────────────┘ └──────────────────────┘ └─────────────────────┘
│ │
│ 授权码 + PKCE │
│ (accesstoken, idtoken) │
└──────────────────────────────────────────────────────────┘
授权头中的 Bearer 令牌
关键约束: MSAL.js 在 Cloudflare Workers 中无法运行(依赖浏览器/Node.js API)。请使用 jose 库进行后端令牌验证。
快速开始
1. 安装依赖
bash
前端 (React SPA)
npm install @azure/msal-react @azure/msal-browser
后端 (Cloudflare Workers)
npm install jose
2. Azure 门户设置
- 1. 前往 Microsoft Entra ID → 应用注册 → 新注册
- 将 重定向 URI 设置为 http://localhost:5173(SPA 类型)
- 记下 应用程序(客户端)ID 和 目录(租户)ID
- 在 身份验证 下:
- 启用
访问令牌 和
ID 令牌
- 添加生产环境重定向 URI
- 5. 在 API 权限 下:
- 添加 User.Read(Microsoft Graph)
- 如有需要,授予管理员同意
前端:MSAL React 设置
配置 (src/auth/msal-config.ts)
typescript
import { Configuration, LogLevel } from @azure/msal-browser;
export const msalConfig: Configuration = {
auth: {
clientId: import.meta.env.VITEAZURECLIENT_ID,
authority: https://login.microsoftonline.com/${import.meta.env.VITEAZURETENANT_ID},
redirectUri: window.location.origin,
postLogoutRedirectUri: window.location.origin,
navigateToLoginRequestUrl: true,
},
cache: {
cacheLocation: localStorage, // 或 sessionStorage
storeAuthStateInCookie: true, // 解决 Safari/Edge 问题所需
},
system: {
loggerOptions: {
logLevel: LogLevel.Warning,
loggerCallback: (level, message) => {
if (level === LogLevel.Error) console.error(message);
},
},
},
};
// 令牌请求的作用域
export const loginRequest = {
scopes: [User.Read, openid, profile, email],
};
// API 调用的作用域(在此处添加您的 API 作用域)
export const apiRequest = {
scopes: [api://${import.meta.env.VITEAZURECLIENTID}/accessas_user],
};
MsalProvider 设置 (src/main.tsx)
typescript
import React from react;
import ReactDOM from react-dom/client;
import { PublicClientApplication, EventType } from @azure/msal-browser;
import { MsalProvider } from @azure/msal-react;
import { msalConfig } from ./auth/msal-config;
import App from ./App;
// 关键:在组件树外部初始化 MSAL,防止重复实例化
const msalInstance = new PublicClientApplication(msalConfig);
// 处理页面加载时的重定向 promise
msalInstance.initialize().then(() => {
// 重定向后设置活动账户
// 重要:使用 getAllAccounts()(返回数组),而不是 getActiveAccount()(返回单个账户或 null)
const accounts = msalInstance.getAllAccounts();
if (accounts.length > 0) {
msalInstance.setActiveAccount(accounts[0]);
}
// 监听登录事件
msalInstance.addEventCallback((event) => {
if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) {
const account = (event.payload as { account: any }).account;
msalInstance.setActiveAccount(account);
}
});
ReactDOM.createRoot(document.getElementById(root)!).render(
);
});
受保护路由组件
typescript
import { useMsal, useIsAuthenticated } from @azure/msal-react;
import { InteractionStatus } from @azure/msal-browser;
import { loginRequest } from ./msal-config;
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { instance, inProgress } = useMsal();
const isAuthenticated = useIsAuthenticated();
// 等待 MSAL 完成任何正在进行的操作
if (inProgress !== InteractionStatus.None) {
return
加载中...
;
}
if (!isAuthenticated) {
// 触发登录重定向
instance.loginRedirect(loginRequest);
return
正在重定向到登录页面...
;
}
return <>{children}>;
}
获取 API 调用令牌
typescript
import { useMsal } from @azure/msal-react;
import { InteractionRequiredAuthError } from @azure/msal-browser;
import { apiRequest } from ./msal-config;
export function useApiToken() {
const { instance, accounts } = useMsal();
async function getAccessToken(): Promise {
if (accounts.length === 0) return null;
const request = {
...apiRequest,
account: accounts[0],
};
try {
// 首先尝试静默获取令牌
const response = await instance.acquireTokenSilent(request);
return response.accessToken;
} catch (error) {
if (error instanceof InteractionRequiredAuthError) {
// 静默获取失败,需要交互式登录
// 这处理过期的刷新令牌 (AADSTS700084)
await instance.acquireTokenRedirect(request);
return null;
}
throw error;
}
}
return { getAccessToken };
}
后端:Cloudflare Workers JWT 验证
为什么使用 jose 而不是 MSAL
MSAL.js 依赖浏览器 API(localStorage、sessionStorage)和 Node.js 加密模块,这些在 Cloudflare Workers 的 V8 隔离运行时中不存在。jose 库是纯 JavaScript,可以在 Workers 中完美运行。
JWT 验证 (src/auth/validate-token.ts)
typescript
import * as jose from jose;
interface EntraTokenPayload {
aud: string; // 受众(您的客户端 ID 或 API URI)
iss: string; // 颁发者(https://login.microsoftonline.com/{tenant}/v2.0)
sub: string; // 主题(用户的唯一 ID)
oid: string; // 对象 ID(用户的 Azure AD 对象 ID)
preferred_username: string;
name: string;
email?: string;
roles?: string[]; // 如果配置了应用角色
scp?: string; // 作用域(空格分隔)
}
// 缓存 JWKS,避免每次请求都获取
let jwksCache: jose.JWTVerifyGetKey | null = null;
let jwksCacheTime = 0;
const JWKSCACHEDURATION = 3600000; // 1 小时
async function getJWKS(tenantId: string): Promise {
const now = Date.now();
if (jwksCache && now - jwksCacheTime < JWKSCACHEDURATION) {
return jwksCache;
}
// 关键:Azure AD JWKS 不在 .well-known/jwks.json 路径
// 必须首先从 openid-configuration 获取
const configUrl = https://login.microsoftonline.com/${tenantId}/v2.0/.well-known/openid-configuration;
const configResponse = await fetch(configUrl);
const config = await configResponse.json()