azure-auth by jezweb/claude-skills
npx skills add https://github.com/jezweb/claude-skills --skill azure-auth包版本 : @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 validation│
└─────────────────────┘ └──────────────────────┘ └─────────────────────┘
│ │
│ Authorization Code + PKCE │
│ (access_token, id_token) │
└──────────────────────────────────────────────────────────┘
Bearer token in Authorization header
关键限制 : MSAL.js 在 Cloudflare Workers 中无法工作(依赖浏览器/Node.js API)。在后端令牌验证中使用 jose 库。
# Frontend (React SPA)
npm install @azure/msal-react @azure/msal-browser
# Backend (Cloudflare Workers)
npm install jose
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
http://localhost:5173 (SPA 类型)User.Read (Microsoft Graph)import { Configuration, LogLevel } from "@azure/msal-browser";
export const msalConfig: Configuration = {
auth: {
clientId: import.meta.env.VITE_AZURE_CLIENT_ID,
authority: `https://login.microsoftonline.com/${import.meta.env.VITE_AZURE_TENANT_ID}`,
redirectUri: window.location.origin,
postLogoutRedirectUri: window.location.origin,
navigateToLoginRequestUrl: true,
},
cache: {
cacheLocation: "localStorage", // or "sessionStorage"
storeAuthStateInCookie: true, // Required for Safari/Edge issues
},
system: {
loggerOptions: {
logLevel: LogLevel.Warning,
loggerCallback: (level, message) => {
if (level === LogLevel.Error) console.error(message);
},
},
},
};
// Scopes for token requests
export const loginRequest = {
scopes: ["User.Read", "openid", "profile", "email"],
};
// Scopes for API calls (add your API scope here)
export const apiRequest = {
scopes: [`api://${import.meta.env.VITE_AZURE_CLIENT_ID}/access_as_user`],
};
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";
// CRITICAL: Initialize MSAL outside component tree to prevent re-instantiation
const msalInstance = new PublicClientApplication(msalConfig);
// Handle redirect promise on page load
msalInstance.initialize().then(() => {
// Set active account after redirect
// IMPORTANT: Use getAllAccounts() (returns array), NOT getActiveAccount() (returns single account or null)
const accounts = msalInstance.getAllAccounts();
if (accounts.length > 0) {
msalInstance.setActiveAccount(accounts[0]);
}
// Listen for sign-in events
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(
<React.StrictMode>
<MsalProvider instance={msalInstance}>
<App />
</MsalProvider>
</React.StrictMode>
);
});
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();
// Wait for MSAL to finish any in-progress operations
if (inProgress !== InteractionStatus.None) {
return <div>Loading...</div>;
}
if (!isAuthenticated) {
// Trigger login redirect
instance.loginRedirect(loginRequest);
return <div>Redirecting to login...</div>;
}
return <>{children}</>;
}
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<string | null> {
if (accounts.length === 0) return null;
const request = {
...apiRequest,
account: accounts[0],
};
try {
// Try silent token acquisition first
const response = await instance.acquireTokenSilent(request);
return response.accessToken;
} catch (error) {
if (error instanceof InteractionRequiredAuthError) {
// Silent acquisition failed, need interactive login
// This handles expired refresh tokens (AADSTS700084)
await instance.acquireTokenRedirect(request);
return null;
}
throw error;
}
}
return { getAccessToken };
}
MSAL.js 依赖浏览器 API(localStorage、sessionStorage)和 Node.js 加密模块,这些在 Cloudflare Workers 的 V8 隔离运行时中不存在。jose 库是纯 JavaScript,在 Workers 中完美运行。
import * as jose from "jose";
interface EntraTokenPayload {
aud: string; // Audience (your client ID or API URI)
iss: string; // Issuer (https://login.microsoftonline.com/{tenant}/v2.0)
sub: string; // Subject (user's unique ID)
oid: string; // Object ID (user's Azure AD object ID)
preferred_username: string;
name: string;
email?: string;
roles?: string[]; // App roles if configured
scp?: string; // Scopes (space-separated)
}
// Cache JWKS to avoid fetching on every request
let jwksCache: jose.JWTVerifyGetKey | null = null;
let jwksCacheTime = 0;
const JWKS_CACHE_DURATION = 3600000; // 1 hour
async function getJWKS(tenantId: string): Promise<jose.JWTVerifyGetKey> {
const now = Date.now();
if (jwksCache && now - jwksCacheTime < JWKS_CACHE_DURATION) {
return jwksCache;
}
// CRITICAL: Azure AD JWKS is NOT at .well-known/jwks.json
// Must fetch from openid-configuration first
const configUrl = `https://login.microsoftonline.com/${tenantId}/v2.0/.well-known/openid-configuration`;
const configResponse = await fetch(configUrl);
const config = await configResponse.json() as { jwks_uri: string };
// Now fetch JWKS from the correct URL
jwksCache = jose.createRemoteJWKSet(new URL(config.jwks_uri));
jwksCacheTime = now;
return jwksCache;
}
export async function validateEntraToken(
token: string,
env: {
AZURE_TENANT_ID: string;
AZURE_CLIENT_ID: string;
}
): Promise<EntraTokenPayload | null> {
try {
const jwks = await getJWKS(env.AZURE_TENANT_ID);
const { payload } = await jose.jwtVerify(token, jwks, {
issuer: `https://login.microsoftonline.com/${env.AZURE_TENANT_ID}/v2.0`,
audience: env.AZURE_CLIENT_ID, // or your API URI: api://{client_id}
});
return payload as unknown as EntraTokenPayload;
} catch (error) {
console.error("Token validation failed:", error);
return null;
}
}
import { validateEntraToken } from "./auth/validate-token";
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Skip auth for public routes
const url = new URL(request.url);
if (url.pathname === "/" || url.pathname.startsWith("/public")) {
return handlePublicRoute(request, env);
}
// Extract Bearer token
const authHeader = request.headers.get("Authorization");
if (!authHeader?.startsWith("Bearer ")) {
return new Response(JSON.stringify({ error: "Missing authorization" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
const token = authHeader.slice(7);
const user = await validateEntraToken(token, env);
if (!user) {
return new Response(JSON.stringify({ error: "Invalid token" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
// Add user to request context
const requestWithUser = new Request(request);
// Pass user info downstream (e.g., via headers or context)
return handleProtectedRoute(request, env, user);
},
};
错误 : "发送了静默登录请求,但没有用户登录"
原因 : 当没有缓存用户时调用了 acquireTokenSilent。
修复 :
// Always check for accounts before silent acquisition
const accounts = instance.getAllAccounts();
if (accounts.length === 0) {
// No cached user, trigger interactive login
await instance.loginRedirect(loginRequest);
return;
}
错误 : "刷新令牌是发给单页应用 (SPA) 的,因此具有固定的有限生命周期 1.00:00:00"
原因 : SPA 刷新令牌在 24 小时后过期。无法延长。
修复 :
try {
const response = await instance.acquireTokenSilent(request);
} catch (error) {
if (error instanceof InteractionRequiredAuthError) {
// Refresh token expired, need fresh login
await instance.acquireTokenRedirect(request);
}
}
错误 : 登录页面和应用之间无限重定向。
原因 : React Router v6 可能会剥离包含身份验证响应的哈希片段。
修复 : 使用自定义 NavigationClient:
import { NavigationClient } from "@azure/msal-browser";
import { useNavigate } from "react-router-dom";
class CustomNavigationClient extends NavigationClient {
private navigate: ReturnType<typeof useNavigate>;
constructor(navigate: ReturnType<typeof useNavigate>) {
super();
this.navigate = navigate;
}
async navigateInternal(url: string, options: { noHistory: boolean }) {
const relativePath = url.replace(window.location.origin, "");
if (options.noHistory) {
this.navigate(relativePath, { replace: true });
} else {
this.navigate(relativePath);
}
return false; // Prevent MSAL from doing its own navigation
}
}
// In your App component:
const navigate = useNavigate();
useEffect(() => {
const navigationClient = new CustomNavigationClient(navigate);
instance.setNavigationClient(navigationClient);
}, [instance, navigate]);
错误 : 动态路由中的 no_cached_authority_error。
原因 : 在组件渲染之前 MSAL 实例未正确初始化。
修复 : 在任何路由之前,在 _app.tsx 中初始化 MSAL:
// pages/_app.tsx
import { PublicClientApplication } from "@azure/msal-browser";
import { MsalProvider } from "@azure/msal-react";
import { msalConfig } from "../auth/msal-config";
// Initialize outside component
const msalInstance = new PublicClientApplication(msalConfig);
// Ensure initialization completes before render
export default function App({ Component, pageProps }) {
const [isInitialized, setIsInitialized] = useState(false);
useEffect(() => {
msalInstance.initialize().then(() => setIsInitialized(true));
}, []);
if (!isInitialized) return <div>Loading...</div>;
return (
<MsalProvider instance={msalInstance}>
<Component {...pageProps} />
</MsalProvider>
);
}
错误 : 身份验证状态丢失,Safari 或 Edge 上无限循环。特别是在 iOS 18 Safari 上,即使启用了第三方 Cookie,静默令牌刷新也会失败并出现 AADSTS50058。
来源 : GitHub Issue #7384
原因 : 这些浏览器具有更严格的 Cookie 策略,影响会话存储。iOS 18 Safari 不存储 login.microsoftonline.com 所需的会话 Cookie,即使在设置中明确允许了第三方 Cookie。
测试说明 : 在 iOS 18 的 Chrome 中工作,但在 iOS 18 的 Safari 中失败。
修复 : 在 MSAL 配置中启用 Cookie 存储:
cache: {
cacheLocation: "localStorage",
storeAuthStateInCookie: true, // REQUIRED for Safari/Edge
}
iOS 18 Safari 限制 : 如果用户在启用 Cookie 存储后仍在 iOS 18 Safari 上遇到问题,这是一个已知的浏览器限制,目前没有解决方法。建议在 iOS 上使用 Chrome 或桌面浏览器。
错误 : 无法从 .well-known/jwks.json 获取 JWKS。
原因 : Azure AD 不在标准的 OpenID Connect 路径上提供 JWKS。
修复 : 首先获取 openid-configuration,然后使用 jwks_uri:
// WRONG - Azure AD doesn't use this path
const jwks = createRemoteJWKSet(
new URL(`https://login.microsoftonline.com/${tenantId}/.well-known/jwks.json`)
);
// CORRECT - Fetch config first
const config = await fetch(
`https://login.microsoftonline.com/${tenantId}/v2.0/.well-known/openid-configuration`
).then(r => r.json());
const jwks = createRemoteJWKSet(new URL(config.jwks_uri));
错误 : 在 React Router loaders 中使用 acquireTokenSilent 时,React 警告在渲染期间更新状态。
来源 : GitHub Issue #7068
原因 : 在路由器 loader 和 MsalProvider 中使用相同的 PublicClientApplication 实例会导致在渲染期间更新状态。
修复 : 在 loader 中再次调用 initialize():
const protectedLoader = async () => {
await msalInstance.initialize(); // Prevents state conflict
const response = await msalInstance.acquireTokenSilent(request);
return { data };
};
错误 : 使用 useMsal() 的组件在调用 setActiveAccount() 后不更新。
来源 : GitHub Issue #6989
已验证 : 多个用户在 GitHub issue 中确认
原因 : setActiveAccount() 更新 MSAL 实例但不通知 React 更改。
修复 : 使用状态强制重新渲染:
const [accountKey, setAccountKey] = useState(0);
const switchAccount = (newAccount) => {
msalInstance.setActiveAccount(newAccount);
setAccountKey(prev => prev + 1); // Force update
};
authority: `https://login.microsoftonline.com/${TENANT_ID}`,
https://login.microsoftonline.com/{tenant_id}/v2.0authority: "https://login.microsoftonline.com/common",
// or for work/school accounts only:
authority: "https://login.microsoftonline.com/organizations",
来自任何 Azure AD 租户的用户都可以登录
令牌颁发者因用户的租户而异
后端验证必须处理多个颁发者:
// Multi-tenant issuer validation
const tenantId = payload.tid; // Tenant ID from token
const expectedIssuer = https://login.microsoftonline.com/${tenantId}/v2.0;
if (payload.iss !== expectedIssuer) {
throw new Error("Invalid issuer");
}
VITE_AZURE_CLIENT_ID=your-client-id-guid
VITE_AZURE_TENANT_ID=your-tenant-id-guid
{
"name": "my-api",
"vars": {
"AZURE_TENANT_ID": "your-tenant-id-guid",
"AZURE_CLIENT_ID": "your-client-id-guid"
}
}
时间线 :
来源 : Microsoft Q&A
现有 B2C 客户 : 可以继续使用 B2C 直到 2030 年,但应计划迁移到 Entra External ID。
新项目 : 对于消费者/客户身份场景,使用 Microsoft Entra External ID。
迁移状态 : 截至 2026 年 1 月,自动化迁移工具处于测试阶段。手动迁移指南可在 Microsoft Learn 获取。
迁移路径 :
{tenant}.ciamlogin.com vs {tenant}.b2clogin.com)参见: https://learn.microsoft.com/en-us/entra/external-id/ 迁移指南: https://learn.microsoft.com/en-us/entra/external-id/customers/how-to-migrate-users
状态 : Azure AD 身份验证库 (ADAL) 已于 2025 年 9 月 30 日退役。使用 ADAL 的应用不再接收安全更新。
如果您正在从 ADAL 迁移 :
关键迁移变更 :
// ADAL (已弃用) - 基于资源
acquireToken({ resource: "https://graph.microsoft.com" })
// MSAL (当前) - 基于范围
acquireTokenSilent({ scopes: ["https://graph.microsoft.com/User.Read"] })
每周安装
327
仓库
GitHub Stars
652
首次出现
2026年1月20日
安全审计
安装于
claude-code273
opencode221
gemini-cli220
cursor205
antigravity197
codex193
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
┌─────────────────────┐ ┌──────────────────────┐ ┌─────────────────────┐
│ React SPA │────▶│ Microsoft Entra ID │────▶│ Cloudflare Worker │
│ @azure/msal-react │ │ (login.microsoft) │ │ jose JWT validation│
└─────────────────────┘ └──────────────────────┘ └─────────────────────┘
│ │
│ Authorization Code + PKCE │
│ (access_token, id_token) │
└──────────────────────────────────────────────────────────┘
Bearer token in Authorization header
Key Constraint : MSAL.js does NOT work in Cloudflare Workers (relies on browser/Node.js APIs). Use jose library for backend token validation.
# Frontend (React SPA)
npm install @azure/msal-react @azure/msal-browser
# Backend (Cloudflare Workers)
npm install jose
http://localhost:5173 (SPA type)User.Read (Microsoft Graph)import { Configuration, LogLevel } from "@azure/msal-browser";
export const msalConfig: Configuration = {
auth: {
clientId: import.meta.env.VITE_AZURE_CLIENT_ID,
authority: `https://login.microsoftonline.com/${import.meta.env.VITE_AZURE_TENANT_ID}`,
redirectUri: window.location.origin,
postLogoutRedirectUri: window.location.origin,
navigateToLoginRequestUrl: true,
},
cache: {
cacheLocation: "localStorage", // or "sessionStorage"
storeAuthStateInCookie: true, // Required for Safari/Edge issues
},
system: {
loggerOptions: {
logLevel: LogLevel.Warning,
loggerCallback: (level, message) => {
if (level === LogLevel.Error) console.error(message);
},
},
},
};
// Scopes for token requests
export const loginRequest = {
scopes: ["User.Read", "openid", "profile", "email"],
};
// Scopes for API calls (add your API scope here)
export const apiRequest = {
scopes: [`api://${import.meta.env.VITE_AZURE_CLIENT_ID}/access_as_user`],
};
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";
// CRITICAL: Initialize MSAL outside component tree to prevent re-instantiation
const msalInstance = new PublicClientApplication(msalConfig);
// Handle redirect promise on page load
msalInstance.initialize().then(() => {
// Set active account after redirect
// IMPORTANT: Use getAllAccounts() (returns array), NOT getActiveAccount() (returns single account or null)
const accounts = msalInstance.getAllAccounts();
if (accounts.length > 0) {
msalInstance.setActiveAccount(accounts[0]);
}
// Listen for sign-in events
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(
<React.StrictMode>
<MsalProvider instance={msalInstance}>
<App />
</MsalProvider>
</React.StrictMode>
);
});
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();
// Wait for MSAL to finish any in-progress operations
if (inProgress !== InteractionStatus.None) {
return <div>Loading...</div>;
}
if (!isAuthenticated) {
// Trigger login redirect
instance.loginRedirect(loginRequest);
return <div>Redirecting to login...</div>;
}
return <>{children}</>;
}
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<string | null> {
if (accounts.length === 0) return null;
const request = {
...apiRequest,
account: accounts[0],
};
try {
// Try silent token acquisition first
const response = await instance.acquireTokenSilent(request);
return response.accessToken;
} catch (error) {
if (error instanceof InteractionRequiredAuthError) {
// Silent acquisition failed, need interactive login
// This handles expired refresh tokens (AADSTS700084)
await instance.acquireTokenRedirect(request);
return null;
}
throw error;
}
}
return { getAccessToken };
}
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.
import * as jose from "jose";
interface EntraTokenPayload {
aud: string; // Audience (your client ID or API URI)
iss: string; // Issuer (https://login.microsoftonline.com/{tenant}/v2.0)
sub: string; // Subject (user's unique ID)
oid: string; // Object ID (user's Azure AD object ID)
preferred_username: string;
name: string;
email?: string;
roles?: string[]; // App roles if configured
scp?: string; // Scopes (space-separated)
}
// Cache JWKS to avoid fetching on every request
let jwksCache: jose.JWTVerifyGetKey | null = null;
let jwksCacheTime = 0;
const JWKS_CACHE_DURATION = 3600000; // 1 hour
async function getJWKS(tenantId: string): Promise<jose.JWTVerifyGetKey> {
const now = Date.now();
if (jwksCache && now - jwksCacheTime < JWKS_CACHE_DURATION) {
return jwksCache;
}
// CRITICAL: Azure AD JWKS is NOT at .well-known/jwks.json
// Must fetch from openid-configuration first
const configUrl = `https://login.microsoftonline.com/${tenantId}/v2.0/.well-known/openid-configuration`;
const configResponse = await fetch(configUrl);
const config = await configResponse.json() as { jwks_uri: string };
// Now fetch JWKS from the correct URL
jwksCache = jose.createRemoteJWKSet(new URL(config.jwks_uri));
jwksCacheTime = now;
return jwksCache;
}
export async function validateEntraToken(
token: string,
env: {
AZURE_TENANT_ID: string;
AZURE_CLIENT_ID: string;
}
): Promise<EntraTokenPayload | null> {
try {
const jwks = await getJWKS(env.AZURE_TENANT_ID);
const { payload } = await jose.jwtVerify(token, jwks, {
issuer: `https://login.microsoftonline.com/${env.AZURE_TENANT_ID}/v2.0`,
audience: env.AZURE_CLIENT_ID, // or your API URI: api://{client_id}
});
return payload as unknown as EntraTokenPayload;
} catch (error) {
console.error("Token validation failed:", error);
return null;
}
}
import { validateEntraToken } from "./auth/validate-token";
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Skip auth for public routes
const url = new URL(request.url);
if (url.pathname === "/" || url.pathname.startsWith("/public")) {
return handlePublicRoute(request, env);
}
// Extract Bearer token
const authHeader = request.headers.get("Authorization");
if (!authHeader?.startsWith("Bearer ")) {
return new Response(JSON.stringify({ error: "Missing authorization" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
const token = authHeader.slice(7);
const user = await validateEntraToken(token, env);
if (!user) {
return new Response(JSON.stringify({ error: "Invalid token" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
// Add user to request context
const requestWithUser = new Request(request);
// Pass user info downstream (e.g., via headers or context)
return handleProtectedRoute(request, env, user);
},
};
Error : "A silent sign-in request was sent but no user is signed in"
Cause : acquireTokenSilent called when no cached user exists.
Fix :
// Always check for accounts before silent acquisition
const accounts = instance.getAllAccounts();
if (accounts.length === 0) {
// No cached user, trigger interactive login
await instance.loginRedirect(loginRequest);
return;
}
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 :
try {
const response = await instance.acquireTokenSilent(request);
} catch (error) {
if (error instanceof InteractionRequiredAuthError) {
// Refresh token expired, need fresh login
await instance.acquireTokenRedirect(request);
}
}
Error : Infinite redirects between login page and app.
Cause : React Router v6 may strip the hash fragment containing auth response.
Fix : Use custom NavigationClient:
import { NavigationClient } from "@azure/msal-browser";
import { useNavigate } from "react-router-dom";
class CustomNavigationClient extends NavigationClient {
private navigate: ReturnType<typeof useNavigate>;
constructor(navigate: ReturnType<typeof useNavigate>) {
super();
this.navigate = navigate;
}
async navigateInternal(url: string, options: { noHistory: boolean }) {
const relativePath = url.replace(window.location.origin, "");
if (options.noHistory) {
this.navigate(relativePath, { replace: true });
} else {
this.navigate(relativePath);
}
return false; // Prevent MSAL from doing its own navigation
}
}
// In your App component:
const navigate = useNavigate();
useEffect(() => {
const navigationClient = new CustomNavigationClient(navigate);
instance.setNavigationClient(navigationClient);
}, [instance, navigate]);
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:
// pages/_app.tsx
import { PublicClientApplication } from "@azure/msal-browser";
import { MsalProvider } from "@azure/msal-react";
import { msalConfig } from "../auth/msal-config";
// Initialize outside component
const msalInstance = new PublicClientApplication(msalConfig);
// Ensure initialization completes before render
export default function App({ Component, pageProps }) {
const [isInitialized, setIsInitialized] = useState(false);
useEffect(() => {
msalInstance.initialize().then(() => setIsInitialized(true));
}, []);
if (!isInitialized) return <div>Loading...</div>;
return (
<MsalProvider instance={msalInstance}>
<Component {...pageProps} />
</MsalProvider>
);
}
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:
cache: {
cacheLocation: "localStorage",
storeAuthStateInCookie: true, // REQUIRED for Safari/Edge
}
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.
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:
// WRONG - Azure AD doesn't use this path
const jwks = createRemoteJWKSet(
new URL(`https://login.microsoftonline.com/${tenantId}/.well-known/jwks.json`)
);
// CORRECT - Fetch config first
const config = await fetch(
`https://login.microsoftonline.com/${tenantId}/v2.0/.well-known/openid-configuration`
).then(r => r.json());
const jwks = createRemoteJWKSet(new URL(config.jwks_uri));
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:
const protectedLoader = async () => {
await msalInstance.initialize(); // Prevents state conflict
const response = await msalInstance.acquireTokenSilent(request);
return { data };
};
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
};
authority: `https://login.microsoftonline.com/${TENANT_ID}`,
https://login.microsoftonline.com/{tenant_id}/v2.0authority: "https://login.microsoftonline.com/common",
// or for work/school accounts only:
authority: "https://login.microsoftonline.com/organizations",
Users from any Azure AD tenant can sign in
Token issuer varies by user's tenant
Backend validation must handle multiple issuers :
// Multi-tenant issuer validation
const tenantId = payload.tid; // Tenant ID from token
const expectedIssuer = https://login.microsoftonline.com/${tenantId}/v2.0;
if (payload.iss !== expectedIssuer) {
throw new Error("Invalid issuer");
}
VITE_AZURE_CLIENT_ID=your-client-id-guid
VITE_AZURE_TENANT_ID=your-tenant-id-guid
{
"name": "my-api",
"vars": {
"AZURE_TENANT_ID": "your-tenant-id-guid",
"AZURE_CLIENT_ID": "your-client-id-guid"
}
}
Timeline :
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 :
{tenant}.ciamlogin.com vs {tenant}.b2clogin.com)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
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 :
Key Migration Changes :
// ADAL (deprecated) - resource-based
acquireToken({ resource: "https://graph.microsoft.com" })
// MSAL (current) - scope-based
acquireTokenSilent({ scopes: ["https://graph.microsoft.com/User.Read"] })
See: https://learn.microsoft.com/en-us/entra/msal/javascript/migration/msal-net-migration
Weekly Installs
327
Repository
GitHub Stars
652
First Seen
Jan 20, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
claude-code273
opencode221
gemini-cli220
cursor205
antigravity197
codex193
Supabase Postgres 最佳实践指南 - 8大类别性能优化规则与SQL示例
55,700 周安装