[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6G6BQT0KECZF45RBY5AP0CF":3,"skill-same:01M6G6BQT0KECZF45RBY5AP0CF":119},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":25,"publishedAt":46,"linkOnly":47,"markdownContent":48,"markdownBody":49,"frontMatter":50,"refName":53,"commitSha":54,"skillDirectory":55,"forksCount":56,"repositoryHtmlUrl":57,"repositoryHomepageUrl":6,"ownerHtmlUrl":58,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":59,"install":60,"resources":72},"01M6G6BQT0KECZF45RBY5AP0CF","azure-auth",null,"Microsoft Entra ID (Azure AD) authentication for React SPAs with MSAL.js and Cloudflare Workers JWT validation using jose library. Full-stack pattern with Authorization Code Flow + PKCE. Prevents 8 documented errors.\n\nUse when: implementing Microsoft SSO, troubleshooting AADSTS50058 loops, AADSTS700084 refresh token errors, React Router redirects, setActiveAccount re-render issues, or validating Entra ID tokens in Workers.","kbarbel640-del\u002Fskills","kbarbel640-del","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F255315490?v=4","01M6CJPPQGCCESXMG3K7BBGQ5J","01M6CJPPX2PB2BCZERFVM2C2B3",1,"MIT","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fveeramanikandanr48\u002Fazure-auth\u002FSKILL.md",[17,22],{"categoryNo":18,"categoryName":19,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000009","前端开发","SCENARIO","使用场景",{"categoryNo":23,"categoryName":24,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000010","后端与接口",[26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45],"auth","azure","azure auth","errors","react","re","render","loops","token","pattern","cloudflare workers","router","stack","flow","library","cloudflare","issues","authentication","code","entra id","2026-11-10 10:12:30",false,"---\nname: azure-auth\ndescription: |\n  Microsoft Entra ID (Azure AD) authentication for React SPAs with MSAL.js and Cloudflare Workers JWT validation using jose library. Full-stack pattern with Authorization Code Flow + PKCE. Prevents 8 documented errors.\n\n  Use when: implementing Microsoft SSO, troubleshooting AADSTS50058 loops, AADSTS700084 refresh token errors, React Router redirects, setActiveAccount re-render issues, or validating Entra ID tokens in Workers.\nuser-invocable: true\n---\n\n# Azure Auth - Microsoft Entra ID for React + Cloudflare Workers\n\n**Package Versions**: @azure\u002Fmsal-react@5.0.2, @azure\u002Fmsal-browser@5.0.2, jose@6.1.3\n**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)\n**Last Updated**: 2026-01-21\n\n---\n\n## Architecture Overview\n\n```\n┌─────────────────────┐     ┌──────────────────────┐     ┌─────────────────────┐\n│   React SPA         │────▶│  Microsoft Entra ID  │────▶│  Cloudflare Worker  │\n│   @azure\u002Fmsal-react │     │  (login.microsoft)   │     │  jose JWT validation│\n└─────────────────────┘     └──────────────────────┘     └─────────────────────┘\n        │                                                          │\n        │  Authorization Code + PKCE                               │\n        │  (access_token, id_token)                                │\n        └──────────────────────────────────────────────────────────┘\n                    Bearer token in Authorization header\n```\n\n**Key Constraint**: MSAL.js does NOT work in Cloudflare Workers (relies on browser\u002FNode.js APIs). Use jose library for backend token validation.\n\n---\n\n## Quick Start\n\n### 1. Install Dependencies\n\n```bash\n# Frontend (React SPA)\nnpm install @azure\u002Fmsal-react @azure\u002Fmsal-browser\n\n# Backend (Cloudflare Workers)\nnpm install jose\n```\n\n### 2. Azure Portal Setup\n\n1. Go to **Microsoft Entra ID** → **App registrations** → **New registration**\n2. Set **Redirect URI** to `http:\u002F\u002Flocalhost:5173` (SPA type)\n3. Note the **Application (client) ID** and **Directory (tenant) ID**\n4. Under **Authentication**:\n   - Enable **Access tokens** and **ID tokens**\n   - Add production redirect URI\n5. Under **API permissions**:\n   - Add `User.Read` (Microsoft Graph)\n   - Grant admin consent if required\n\n---\n\n## Frontend: MSAL React Setup\n\n### Configuration (src\u002Fauth\u002Fmsal-config.ts)\n\n```typescript\nimport { Configuration, LogLevel } from \"@azure\u002Fmsal-browser\";\n\nexport const msalConfig: Configuration = {\n  auth: {\n    clientId: import.meta.env.VITE_AZURE_CLIENT_ID,\n    authority: `https:\u002F\u002Flogin.microsoftonline.com\u002F${import.meta.env.VITE_AZURE_TENANT_ID}`,\n    redirectUri: window.location.origin,\n    postLogoutRedirectUri: window.location.origin,\n    navigateToLoginRequestUrl: true,\n  },\n  cache: {\n    cacheLocation: \"localStorage\", \u002F\u002F or \"sessionStorage\"\n    storeAuthStateInCookie: true, \u002F\u002F Required for Safari\u002FEdge issues\n  },\n  system: {\n    loggerOptions: {\n      logLevel: LogLevel.Warning,\n      loggerCallback: (level, message) => {\n        if (level === LogLevel.Error) console.error(message);\n      },\n    },\n  },\n};\n\n\u002F\u002F Scopes for token requests\nexport const loginRequest = {\n  scopes: [\"User.Read\", \"openid\", \"profile\", \"email\"],\n};\n\n\u002F\u002F Scopes for API calls (add your API scope here)\nexport const apiRequest = {\n  scopes: [`api:\u002F\u002F${import.meta.env.VITE_AZURE_CLIENT_ID}\u002Faccess_as_user`],\n};\n```\n\n### MsalProvider Setup (src\u002Fmain.tsx)\n\n```typescript\nimport React from \"react\";\nimport ReactDOM from \"react-dom\u002Fclient\";\nimport { PublicClientApplication, EventType } from \"@azure\u002Fmsal-browser\";\nimport { MsalProvider } from \"@azure\u002Fmsal-react\";\nimport { msalConfig } from \".\u002Fauth\u002Fmsal-config\";\nimport App from \".\u002FApp\";\n\n\u002F\u002F CRITICAL: Initialize MSAL outside component tree to prevent re-instantiation\nconst msalInstance = new PublicClientApplication(msalConfig);\n\n\u002F\u002F Handle redirect promise on page load\nmsalInstance.initialize().then(() => {\n  \u002F\u002F Set active account after redirect\n  \u002F\u002F IMPORTANT: Use getAllAccounts() (returns array), NOT getActiveAccount() (returns single account or null)\n  const accounts = msalInstance.getAllAccounts();\n  if (accounts.length > 0) {\n    msalInstance.setActiveAccount(accounts[0]);\n  }\n\n  \u002F\u002F Listen for sign-in events\n  msalInstance.addEventCallback((event) => {\n    if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) {\n      const account = (event.payload as { account: any }).account;\n      msalInstance.setActiveAccount(account);\n    }\n  });\n\n  ReactDOM.createRoot(document.getElementById(\"root\")!).render(\n    \u003CReact.StrictMode>\n      \u003CMsalProvider instance={msalInstance}>\n        \u003CApp \u002F>\n      \u003C\u002FMsalProvider>\n    \u003C\u002FReact.StrictMode>\n  );\n});\n```\n\n### Protected Route Component\n\n```typescript\nimport { useMsal, useIsAuthenticated } from \"@azure\u002Fmsal-react\";\nimport { InteractionStatus } from \"@azure\u002Fmsal-browser\";\nimport { loginRequest } from \".\u002Fmsal-config\";\n\nexport function ProtectedRoute({ children }: { children: React.ReactNode }) {\n  const { instance, inProgress } = useMsal();\n  const isAuthenticated = useIsAuthenticated();\n\n  \u002F\u002F Wait for MSAL to finish any in-progress operations\n  if (inProgress !== InteractionStatus.None) {\n    return \u003Cdiv>Loading...\u003C\u002Fdiv>;\n  }\n\n  if (!isAuthenticated) {\n    \u002F\u002F Trigger login redirect\n    instance.loginRedirect(loginRequest);\n    return \u003Cdiv>Redirecting to login...\u003C\u002Fdiv>;\n  }\n\n  return \u003C>{children}\u003C\u002F>;\n}\n```\n\n### Acquiring Tokens for API Calls\n\n```typescript\nimport { useMsal } from \"@azure\u002Fmsal-react\";\nimport { InteractionRequiredAuthError } from \"@azure\u002Fmsal-browser\";\nimport { apiRequest } from \".\u002Fmsal-config\";\n\nexport function useApiToken() {\n  const { instance, accounts } = useMsal();\n\n  async function getAccessToken(): Promise\u003Cstring | null> {\n    if (accounts.length === 0) return null;\n\n    const request = {\n      ...apiRequest,\n      account: accounts[0],\n    };\n\n    try {\n      \u002F\u002F Try silent token acquisition first\n      const response = await instance.acquireTokenSilent(request);\n      return response.accessToken;\n    } catch (error) {\n      if (error instanceof InteractionRequiredAuthError) {\n        \u002F\u002F Silent acquisition failed, need interactive login\n        \u002F\u002F This handles expired refresh tokens (AADSTS700084)\n        await instance.acquireTokenRedirect(request);\n        return null;\n      }\n      throw error;\n    }\n  }\n\n  return { getAccessToken };\n}\n```\n\n---\n\n## Backend: Cloudflare Workers JWT Validation\n\n### Why jose Instead of MSAL\n\nMSAL.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.\n\n### JWT Validation (src\u002Fauth\u002Fvalidate-token.ts)\n\n```typescript\nimport * as jose from \"jose\";\n\ninterface EntraTokenPayload {\n  aud: string;       \u002F\u002F Audience (your client ID or API URI)\n  iss: string;       \u002F\u002F Issuer (https:\u002F\u002Flogin.microsoftonline.com\u002F{tenant}\u002Fv2.0)\n  sub: string;       \u002F\u002F Subject (user's unique ID)\n  oid: string;       \u002F\u002F Object ID (user's Azure AD object ID)\n  preferred_username: string;\n  name: string;\n  email?: string;\n  roles?: string[];  \u002F\u002F App roles if configured\n  scp?: string;      \u002F\u002F Scopes (space-separated)\n}\n\n\u002F\u002F Cache JWKS to avoid fetching on every request\nlet jwksCache: jose.JWTVerifyGetKey | null = null;\nlet jwksCacheTime = 0;\nconst JWKS_CACHE_DURATION = 3600000; \u002F\u002F 1 hour\n\nasync function getJWKS(tenantId: string): Promise\u003Cjose.JWTVerifyGetKey> {\n  const now = Date.now();\n\n  if (jwksCache && now - jwksCacheTime \u003C JWKS_CACHE_DURATION) {\n    return jwksCache;\n  }\n\n  \u002F\u002F CRITICAL: Azure AD JWKS is NOT at .well-known\u002Fjwks.json\n  \u002F\u002F Must fetch from openid-configuration first\n  const configUrl = `https:\u002F\u002Flogin.microsoftonline.com\u002F${tenantId}\u002Fv2.0\u002F.well-known\u002Fopenid-configuration`;\n  const configResponse = await fetch(configUrl);\n  const config = await configResponse.json() as { jwks_uri: string };\n\n  \u002F\u002F Now fetch JWKS from the correct URL\n  jwksCache = jose.createRemoteJWKSet(new URL(config.jwks_uri));\n  jwksCacheTime = now;\n\n  return jwksCache;\n}\n\nexport async function validateEntraToken(\n  token: string,\n  env: {\n    AZURE_TENANT_ID: string;\n    AZURE_CLIENT_ID: string;\n  }\n): Promise\u003CEntraTokenPayload | null> {\n  try {\n    const jwks = await getJWKS(env.AZURE_TENANT_ID);\n\n    const { payload } = await jose.jwtVerify(token, jwks, {\n      issuer: `https:\u002F\u002Flogin.microsoftonline.com\u002F${env.AZURE_TENANT_ID}\u002Fv2.0`,\n      audience: env.AZURE_CLIENT_ID, \u002F\u002F or your API URI: api:\u002F\u002F{client_id}\n    });\n\n    return payload as unknown as EntraTokenPayload;\n  } catch (error) {\n    console.error(\"Token validation failed:\", error);\n    return null;\n  }\n}\n```\n\n### Worker Middleware Pattern\n\n```typescript\nimport { validateEntraToken } from \".\u002Fauth\u002Fvalidate-token\";\n\nexport default {\n  async fetch(request: Request, env: Env): Promise\u003CResponse> {\n    \u002F\u002F Skip auth for public routes\n    const url = new URL(request.url);\n    if (url.pathname === \"\u002F\" || url.pathname.startsWith(\"\u002Fpublic\")) {\n      return handlePublicRoute(request, env);\n    }\n\n    \u002F\u002F Extract Bearer token\n    const authHeader = request.headers.get(\"Authorization\");\n    if (!authHeader?.startsWith(\"Bearer \")) {\n      return new Response(JSON.stringify({ error: \"Missing authorization\" }), {\n        status: 401,\n        headers: { \"Content-Type\": \"application\u002Fjson\" },\n      });\n    }\n\n    const token = authHeader.slice(7);\n    const user = await validateEntraToken(token, env);\n\n    if (!user) {\n      return new Response(JSON.stringify({ error: \"Invalid token\" }), {\n        status: 401,\n        headers: { \"Content-Type\": \"application\u002Fjson\" },\n      });\n    }\n\n    \u002F\u002F Add user to request context\n    const requestWithUser = new Request(request);\n    \u002F\u002F Pass user info downstream (e.g., via headers or context)\n\n    return handleProtectedRoute(request, env, user);\n  },\n};\n```\n\n---\n\n## Common Errors & Fixes\n\n### 1. AADSTS50058 - Silent Sign-In Loop\n\n**Error**: \"A silent sign-in request was sent but no user is signed in\"\n\n**Cause**: `acquireTokenSilent` called when no cached user exists.\n\n**Fix**:\n```typescript\n\u002F\u002F Always check for accounts before silent acquisition\nconst accounts = instance.getAllAccounts();\nif (accounts.length === 0) {\n  \u002F\u002F No cached user, trigger interactive login\n  await instance.loginRedirect(loginRequest);\n  return;\n}\n```\n\n### 2. AADSTS700084 - Refresh Token Expired\n\n**Error**: \"The refresh token was issued to a single page app (SPA), and therefore has a fixed, limited lifetime of 1.00:00:00\"\n\n**Cause**: SPA refresh tokens expire after 24 hours. Cannot be extended.\n\n**Fix**:\n```typescript\ntry {\n  const response = await instance.acquireTokenSilent(request);\n} catch (error) {\n  if (error instanceof InteractionRequiredAuthError) {\n    \u002F\u002F Refresh token expired, need fresh login\n    await instance.acquireTokenRedirect(request);\n  }\n}\n```\n\n### 3. React Router v6 Redirect Loop\n\n**Error**: Infinite redirects between login page and app.\n\n**Cause**: React Router v6 may strip the hash fragment containing auth response.\n\n**Fix**: Use custom NavigationClient:\n```typescript\nimport { NavigationClient } from \"@azure\u002Fmsal-browser\";\nimport { useNavigate } from \"react-router-dom\";\n\nclass CustomNavigationClient extends NavigationClient {\n  private navigate: ReturnType\u003Ctypeof useNavigate>;\n\n  constructor(navigate: ReturnType\u003Ctypeof useNavigate>) {\n    super();\n    this.navigate = navigate;\n  }\n\n  async navigateInternal(url: string, options: { noHistory: boolean }) {\n    const relativePath = url.replace(window.location.origin, \"\");\n    if (options.noHistory) {\n      this.navigate(relativePath, { replace: true });\n    } else {\n      this.navigate(relativePath);\n    }\n    return false; \u002F\u002F Prevent MSAL from doing its own navigation\n  }\n}\n\n\u002F\u002F In your App component:\nconst navigate = useNavigate();\nuseEffect(() => {\n  const navigationClient = new CustomNavigationClient(navigate);\n  instance.setNavigationClient(navigationClient);\n}, [instance, navigate]);\n```\n\n### 4. NextJS Dynamic Route Error\n\n**Error**: `no_cached_authority_error` in dynamic routes.\n\n**Cause**: MSAL instance not properly initialized before component renders.\n\n**Fix**: Initialize MSAL in `_app.tsx` before any routing:\n```typescript\n\u002F\u002F pages\u002F_app.tsx\nimport { PublicClientApplication } from \"@azure\u002Fmsal-browser\";\nimport { MsalProvider } from \"@azure\u002Fmsal-react\";\nimport { msalConfig } from \"..\u002Fauth\u002Fmsal-config\";\n\n\u002F\u002F Initialize outside component\nconst msalInstance = new PublicClientApplication(msalConfig);\n\n\u002F\u002F Ensure initialization completes before render\nexport default function App({ Component, pageProps }) {\n  const [isInitialized, setIsInitialized] = useState(false);\n\n  useEffect(() => {\n    msalInstance.initialize().then(() => setIsInitialized(true));\n  }, []);\n\n  if (!isInitialized) return \u003Cdiv>Loading...\u003C\u002Fdiv>;\n\n  return (\n    \u003CMsalProvider instance={msalInstance}>\n      \u003CComponent {...pageProps} \u002F>\n    \u003C\u002FMsalProvider>\n  );\n}\n```\n\n### 5. Safari\u002FEdge Cookie Issues\n\n**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.\n\n**Source**: [GitHub Issue #7384](https:\u002F\u002Fgithub.com\u002FAzureAD\u002Fmicrosoft-authentication-library-for-js\u002Fissues\u002F7384)\n\n**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.\n\n**Testing Note**: Works in Chrome on iOS 18, but fails in Safari on iOS 18.\n\n**Fix**: Enable cookie storage in MSAL config:\n```typescript\ncache: {\n  cacheLocation: \"localStorage\",\n  storeAuthStateInCookie: true, \u002F\u002F REQUIRED for Safari\u002FEdge\n}\n```\n\n**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.\n\n### 6. JWKS URL Not Found (Workers)\n\n**Error**: Failed to fetch JWKS from `.well-known\u002Fjwks.json`.\n\n**Cause**: Azure AD doesn't serve JWKS at the standard OpenID Connect path.\n\n**Fix**: Fetch `openid-configuration` first, then use `jwks_uri`:\n```typescript\n\u002F\u002F WRONG - Azure AD doesn't use this path\nconst jwks = createRemoteJWKSet(\n  new URL(`https:\u002F\u002Flogin.microsoftonline.com\u002F${tenantId}\u002F.well-known\u002Fjwks.json`)\n);\n\n\u002F\u002F CORRECT - Fetch config first\nconst config = await fetch(\n  `https:\u002F\u002Flogin.microsoftonline.com\u002F${tenantId}\u002Fv2.0\u002F.well-known\u002Fopenid-configuration`\n).then(r => r.json());\nconst jwks = createRemoteJWKSet(new URL(config.jwks_uri));\n```\n\n### 7. React Router Loader State Conflict\n\n**Error**: React warning about updating state during render when using `acquireTokenSilent` in React Router loaders.\n\n**Source**: [GitHub Issue #7068](https:\u002F\u002Fgithub.com\u002FAzureAD\u002Fmicrosoft-authentication-library-for-js\u002Fissues\u002F7068)\n\n**Cause**: Using the same `PublicClientApplication` instance in both the router loader and `MsalProvider` causes state updates during rendering.\n\n**Fix**: Call `initialize()` again in the loader:\n```typescript\nconst protectedLoader = async () => {\n  await msalInstance.initialize(); \u002F\u002F Prevents state conflict\n  const response = await msalInstance.acquireTokenSilent(request);\n  return { data };\n};\n```\n\n### 8. setActiveAccount Doesn't Trigger Re-render (Community-sourced)\n\n**Error**: Components using `useMsal()` don't update after calling `setActiveAccount()`.\n\n**Source**: [GitHub Issue #6989](https:\u002F\u002Fgithub.com\u002FAzureAD\u002Fmicrosoft-authentication-library-for-js\u002Fissues\u002F6989)\n\n**Verified**: Multiple users confirmed in GitHub issue\n\n**Cause**: `setActiveAccount()` updates the MSAL instance but doesn't notify React of the change.\n\n**Fix**: Force re-render with state:\n```typescript\nconst [accountKey, setAccountKey] = useState(0);\n\nconst switchAccount = (newAccount) => {\n  msalInstance.setActiveAccount(newAccount);\n  setAccountKey(prev => prev + 1); \u002F\u002F Force update\n};\n```\n\n---\n\n## Multi-Tenant vs Single-Tenant\n\n### Single Tenant (Recommended for Enterprise)\n\n```typescript\nauthority: `https:\u002F\u002Flogin.microsoftonline.com\u002F${TENANT_ID}`,\n```\n\n- Only users from your organization can sign in\n- Token issuer: `https:\u002F\u002Flogin.microsoftonline.com\u002F{tenant_id}\u002Fv2.0`\n\n### Multi-Tenant\n\n```typescript\nauthority: \"https:\u002F\u002Flogin.microsoftonline.com\u002Fcommon\",\n\u002F\u002F or for work\u002Fschool accounts only:\nauthority: \"https:\u002F\u002Flogin.microsoftonline.com\u002Forganizations\",\n```\n\n- Users from any Azure AD tenant can sign in\n- Token issuer varies by user's tenant\n- **Backend validation must handle multiple issuers**:\n\n```typescript\n\u002F\u002F Multi-tenant issuer validation\nconst tenantId = payload.tid; \u002F\u002F Tenant ID from token\nconst expectedIssuer = `https:\u002F\u002Flogin.microsoftonline.com\u002F${tenantId}\u002Fv2.0`;\nif (payload.iss !== expectedIssuer) {\n  throw new Error(\"Invalid issuer\");\n}\n```\n\n---\n\n## Environment Variables\n\n### Frontend (.env)\n\n```bash\nVITE_AZURE_CLIENT_ID=your-client-id-guid\nVITE_AZURE_TENANT_ID=your-tenant-id-guid\n```\n\n### Backend (wrangler.jsonc)\n\n```jsonc\n{\n  \"name\": \"my-api\",\n  \"vars\": {\n    \"AZURE_TENANT_ID\": \"your-tenant-id-guid\",\n    \"AZURE_CLIENT_ID\": \"your-client-id-guid\"\n  }\n}\n```\n\n---\n\n## Azure AD B2C Sunset\n\n**Timeline**:\n- **May 1, 2025**: Azure AD B2C no longer available for new customer signups\n- **March 15, 2026**: Azure AD B2C P2 discontinued for all customers\n- **May 2030**: Microsoft will continue supporting existing B2C customers with standard support\n\n**Source**: [Microsoft Q&A](https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fanswers\u002Fquestions\u002F2119363\u002Fmigrating-existing-azure-ad-b2c-to-microsoft-entra)\n\n**Existing B2C Customers**: Can continue using B2C until 2030, but should plan migration to Entra External ID.\n\n**New Projects**: Use **Microsoft Entra External ID** for consumer\u002Fcustomer identity scenarios.\n\n**Migration Status**: As of January 2026, automated migration tools are in testing phase. Manual migration guidance available at Microsoft Learn.\n\n**Migration Path**:\n- Different authority URL format (`{tenant}.ciamlogin.com` vs `{tenant}.b2clogin.com`)\n- Updated SDK support (same MSAL libraries)\n- New pricing model (consumption-based)\n- Self-Service Password Reset (SSPR) approach available for user migration\n- Seamless migration samples on GitHub (preview)\n\nSee: https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fentra\u002Fexternal-id\u002F\nMigration Guide: https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fentra\u002Fexternal-id\u002Fcustomers\u002Fhow-to-migrate-users\n\n---\n\n## ADAL Retirement (Complete)\n\n**Status**: Azure AD Authentication Library (ADAL) was retired on September 30, 2025. Apps using ADAL no longer receive security updates.\n\n**If you're migrating from ADAL**:\n1. ADAL → MSAL migration is required\n2. ADAL used v1.0 endpoints; MSAL uses v2.0 endpoints\n3. Token cache format differs - users must re-authenticate\n4. Scopes replace \"resources\" in token requests\n\n**Key Migration Changes**:\n```typescript\n\u002F\u002F ADAL (deprecated) - resource-based\nacquireToken({ resource: \"https:\u002F\u002Fgraph.microsoft.com\" })\n\n\u002F\u002F MSAL (current) - scope-based\nacquireTokenSilent({ scopes: [\"https:\u002F\u002Fgraph.microsoft.com\u002FUser.Read\"] })\n```\n\nSee: https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fentra\u002Fmsal\u002Fjavascript\u002Fmigration\u002Fmsal-net-migration\n\n---\n\n## Resources\n\n- [MSAL React Documentation](https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fentra\u002Fmsal\u002Fjavascript\u002Freact\u002F)\n- [Microsoft Entra ID App Registration](https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fentra\u002Fidentity-platform\u002Fquickstart-register-app)\n- [MSAL.js GitHub Issues](https:\u002F\u002Fgithub.com\u002FAzureAD\u002Fmicrosoft-authentication-library-for-js\u002Fissues)\n- [jose Library](https:\u002F\u002Fgithub.com\u002Fpanva\u002Fjose)\n- [Cloudflare Workers + Azure AD Blog](https:\u002F\u002Fhajekj.net\u002F2021\u002F11\u002F12\u002Fcloudflare-workers-and-azure-ad\u002F)\n","# Azure Auth - Microsoft Entra ID for React + Cloudflare Workers\n\n**Package Versions**: @azure\u002Fmsal-react@5.0.2, @azure\u002Fmsal-browser@5.0.2, jose@6.1.3\n**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)\n**Last Updated**: 2026-01-21\n\n---\n\n## Architecture Overview\n\n```\n┌─────────────────────┐     ┌──────────────────────┐     ┌─────────────────────┐\n│   React SPA         │────▶│  Microsoft Entra ID  │────▶│  Cloudflare Worker  │\n│   @azure\u002Fmsal-react │     │  (login.microsoft)   │     │  jose JWT validation│\n└─────────────────────┘     └──────────────────────┘     └─────────────────────┘\n        │                                                          │\n        │  Authorization Code + PKCE                               │\n        │  (access_token, id_token)                                │\n        └──────────────────────────────────────────────────────────┘\n                    Bearer token in Authorization header\n```\n\n**Key Constraint**: MSAL.js does NOT work in Cloudflare Workers (relies on browser\u002FNode.js APIs). Use jose library for backend token validation.\n\n---\n\n## Quick Start\n\n### 1. Install Dependencies\n\n```bash\n# Frontend (React SPA)\nnpm install @azure\u002Fmsal-react @azure\u002Fmsal-browser\n\n# Backend (Cloudflare Workers)\nnpm install jose\n```\n\n### 2. Azure Portal Setup\n\n1. Go to **Microsoft Entra ID** → **App registrations** → **New registration**\n2. Set **Redirect URI** to `http:\u002F\u002Flocalhost:5173` (SPA type)\n3. Note the **Application (client) ID** and **Directory (tenant) ID**\n4. Under **Authentication**:\n   - Enable **Access tokens** and **ID tokens**\n   - Add production redirect URI\n5. Under **API permissions**:\n   - Add `User.Read` (Microsoft Graph)\n   - Grant admin consent if required\n\n---\n\n## Frontend: MSAL React Setup\n\n### Configuration (src\u002Fauth\u002Fmsal-config.ts)\n\n```typescript\nimport { Configuration, LogLevel } from \"@azure\u002Fmsal-browser\";\n\nexport const msalConfig: Configuration = {\n  auth: {\n    clientId: import.meta.env.VITE_AZURE_CLIENT_ID,\n    authority: `https:\u002F\u002Flogin.microsoftonline.com\u002F${import.meta.env.VITE_AZURE_TENANT_ID}`,\n    redirectUri: window.location.origin,\n    postLogoutRedirectUri: window.location.origin,\n    navigateToLoginRequestUrl: true,\n  },\n  cache: {\n    cacheLocation: \"localStorage\", \u002F\u002F or \"sessionStorage\"\n    storeAuthStateInCookie: true, \u002F\u002F Required for Safari\u002FEdge issues\n  },\n  system: {\n    loggerOptions: {\n      logLevel: LogLevel.Warning,\n      loggerCallback: (level, message) => {\n        if (level === LogLevel.Error) console.error(message);\n      },\n    },\n  },\n};\n\n\u002F\u002F Scopes for token requests\nexport const loginRequest = {\n  scopes: [\"User.Read\", \"openid\", \"profile\", \"email\"],\n};\n\n\u002F\u002F Scopes for API calls (add your API scope here)\nexport const apiRequest = {\n  scopes: [`api:\u002F\u002F${import.meta.env.VITE_AZURE_CLIENT_ID}\u002Faccess_as_user`],\n};\n```\n\n### MsalProvider Setup (src\u002Fmain.tsx)\n\n```typescript\nimport React from \"react\";\nimport ReactDOM from \"react-dom\u002Fclient\";\nimport { PublicClientApplication, EventType } from \"@azure\u002Fmsal-browser\";\nimport { MsalProvider } from \"@azure\u002Fmsal-react\";\nimport { msalConfig } from \".\u002Fauth\u002Fmsal-config\";\nimport App from \".\u002FApp\";\n\n\u002F\u002F CRITICAL: Initialize MSAL outside component tree to prevent re-instantiation\nconst msalInstance = new PublicClientApplication(msalConfig);\n\n\u002F\u002F Handle redirect promise on page load\nmsalInstance.initialize().then(() => {\n  \u002F\u002F Set active account after redirect\n  \u002F\u002F IMPORTANT: Use getAllAccounts() (returns array), NOT getActiveAccount() (returns single account or null)\n  const accounts = msalInstance.getAllAccounts();\n  if (accounts.length > 0) {\n    msalInstance.setActiveAccount(accounts[0]);\n  }\n\n  \u002F\u002F Listen for sign-in events\n  msalInstance.addEventCallback((event) => {\n    if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) {\n      const account = (event.payload as { account: any }).account;\n      msalInstance.setActiveAccount(account);\n    }\n  });\n\n  ReactDOM.createRoot(document.getElementById(\"root\")!).render(\n    \u003CReact.StrictMode>\n      \u003CMsalProvider instance={msalInstance}>\n        \u003CApp \u002F>\n      \u003C\u002FMsalProvider>\n    \u003C\u002FReact.StrictMode>\n  );\n});\n```\n\n### Protected Route Component\n\n```typescript\nimport { useMsal, useIsAuthenticated } from \"@azure\u002Fmsal-react\";\nimport { InteractionStatus } from \"@azure\u002Fmsal-browser\";\nimport { loginRequest } from \".\u002Fmsal-config\";\n\nexport function ProtectedRoute({ children }: { children: React.ReactNode }) {\n  const { instance, inProgress } = useMsal();\n  const isAuthenticated = useIsAuthenticated();\n\n  \u002F\u002F Wait for MSAL to finish any in-progress operations\n  if (inProgress !== InteractionStatus.None) {\n    return \u003Cdiv>Loading...\u003C\u002Fdiv>;\n  }\n\n  if (!isAuthenticated) {\n    \u002F\u002F Trigger login redirect\n    instance.loginRedirect(loginRequest);\n    return \u003Cdiv>Redirecting to login...\u003C\u002Fdiv>;\n  }\n\n  return \u003C>{children}\u003C\u002F>;\n}\n```\n\n### Acquiring Tokens for API Calls\n\n```typescript\nimport { useMsal } from \"@azure\u002Fmsal-react\";\nimport { InteractionRequiredAuthError } from \"@azure\u002Fmsal-browser\";\nimport { apiRequest } from \".\u002Fmsal-config\";\n\nexport function useApiToken() {\n  const { instance, accounts } = useMsal();\n\n  async function getAccessToken(): Promise\u003Cstring | null> {\n    if (accounts.length === 0) return null;\n\n    const request = {\n      ...apiRequest,\n      account: accounts[0],\n    };\n\n    try {\n      \u002F\u002F Try silent token acquisition first\n      const response = await instance.acquireTokenSilent(request);\n      return response.accessToken;\n    } catch (error) {\n      if (error instanceof InteractionRequiredAuthError) {\n        \u002F\u002F Silent acquisition failed, need interactive login\n        \u002F\u002F This handles expired refresh tokens (AADSTS700084)\n        await instance.acquireTokenRedirect(request);\n        return null;\n      }\n      throw error;\n    }\n  }\n\n  return { getAccessToken };\n}\n```\n\n---\n\n## Backend: Cloudflare Workers JWT Validation\n\n### Why jose Instead of MSAL\n\nMSAL.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.\n\n### JWT Validation (src\u002Fauth\u002Fvalidate-token.ts)\n\n```typescript\nimport * as jose from \"jose\";\n\ninterface EntraTokenPayload {\n  aud: string;       \u002F\u002F Audience (your client ID or API URI)\n  iss: string;       \u002F\u002F Issuer (https:\u002F\u002Flogin.microsoftonline.com\u002F{tenant}\u002Fv2.0)\n  sub: string;       \u002F\u002F Subject (user's unique ID)\n  oid: string;       \u002F\u002F Object ID (user's Azure AD object ID)\n  preferred_username: string;\n  name: string;\n  email?: string;\n  roles?: string[];  \u002F\u002F App roles if configured\n  scp?: string;      \u002F\u002F Scopes (space-separated)\n}\n\n\u002F\u002F Cache JWKS to avoid fetching on every request\nlet jwksCache: jose.JWTVerifyGetKey | null = null;\nlet jwksCacheTime = 0;\nconst JWKS_CACHE_DURATION = 3600000; \u002F\u002F 1 hour\n\nasync function getJWKS(tenantId: string): Promise\u003Cjose.JWTVerifyGetKey> {\n  const now = Date.now();\n\n  if (jwksCache && now - jwksCacheTime \u003C JWKS_CACHE_DURATION) {\n    return jwksCache;\n  }\n\n  \u002F\u002F CRITICAL: Azure AD JWKS is NOT at .well-known\u002Fjwks.json\n  \u002F\u002F Must fetch from openid-configuration first\n  const configUrl = `https:\u002F\u002Flogin.microsoftonline.com\u002F${tenantId}\u002Fv2.0\u002F.well-known\u002Fopenid-configuration`;\n  const configResponse = await fetch(configUrl);\n  const config = await configResponse.json() as { jwks_uri: string };\n\n  \u002F\u002F Now fetch JWKS from the correct URL\n  jwksCache = jose.createRemoteJWKSet(new URL(config.jwks_uri));\n  jwksCacheTime = now;\n\n  return jwksCache;\n}\n\nexport async function validateEntraToken(\n  token: string,\n  env: {\n    AZURE_TENANT_ID: string;\n    AZURE_CLIENT_ID: string;\n  }\n): Promise\u003CEntraTokenPayload | null> {\n  try {\n    const jwks = await getJWKS(env.AZURE_TENANT_ID);\n\n    const { payload } = await jose.jwtVerify(token, jwks, {\n      issuer: `https:\u002F\u002Flogin.microsoftonline.com\u002F${env.AZURE_TENANT_ID}\u002Fv2.0`,\n      audience: env.AZURE_CLIENT_ID, \u002F\u002F or your API URI: api:\u002F\u002F{client_id}\n    });\n\n    return payload as unknown as EntraTokenPayload;\n  } catch (error) {\n    console.error(\"Token validation failed:\", error);\n    return null;\n  }\n}\n```\n\n### Worker Middleware Pattern\n\n```typescript\nimport { validateEntraToken } from \".\u002Fauth\u002Fvalidate-token\";\n\nexport default {\n  async fetch(request: Request, env: Env): Promise\u003CResponse> {\n    \u002F\u002F Skip auth for public routes\n    const url = new URL(request.url);\n    if (url.pathname === \"\u002F\" || url.pathname.startsWith(\"\u002Fpublic\")) {\n      return handlePublicRoute(request, env);\n    }\n\n    \u002F\u002F Extract Bearer token\n    const authHeader = request.headers.get(\"Authorization\");\n    if (!authHeader?.startsWith(\"Bearer \")) {\n      return new Response(JSON.stringify({ error: \"Missing authorization\" }), {\n        status: 401,\n        headers: { \"Content-Type\": \"application\u002Fjson\" },\n      });\n    }\n\n    const token = authHeader.slice(7);\n    const user = await validateEntraToken(token, env);\n\n    if (!user) {\n      return new Response(JSON.stringify({ error: \"Invalid token\" }), {\n        status: 401,\n        headers: { \"Content-Type\": \"application\u002Fjson\" },\n      });\n    }\n\n    \u002F\u002F Add user to request context\n    const requestWithUser = new Request(request);\n    \u002F\u002F Pass user info downstream (e.g., via headers or context)\n\n    return handleProtectedRoute(request, env, user);\n  },\n};\n```\n\n---\n\n## Common Errors & Fixes\n\n### 1. AADSTS50058 - Silent Sign-In Loop\n\n**Error**: \"A silent sign-in request was sent but no user is signed in\"\n\n**Cause**: `acquireTokenSilent` called when no cached user exists.\n\n**Fix**:\n```typescript\n\u002F\u002F Always check for accounts before silent acquisition\nconst accounts = instance.getAllAccounts();\nif (accounts.length === 0) {\n  \u002F\u002F No cached user, trigger interactive login\n  await instance.loginRedirect(loginRequest);\n  return;\n}\n```\n\n### 2. AADSTS700084 - Refresh Token Expired\n\n**Error**: \"The refresh token was issued to a single page app (SPA), and therefore has a fixed, limited lifetime of 1.00:00:00\"\n\n**Cause**: SPA refresh tokens expire after 24 hours. Cannot be extended.\n\n**Fix**:\n```typescript\ntry {\n  const response = await instance.acquireTokenSilent(request);\n} catch (error) {\n  if (error instanceof InteractionRequiredAuthError) {\n    \u002F\u002F Refresh token expired, need fresh login\n    await instance.acquireTokenRedirect(request);\n  }\n}\n```\n\n### 3. React Router v6 Redirect Loop\n\n**Error**: Infinite redirects between login page and app.\n\n**Cause**: React Router v6 may strip the hash fragment containing auth response.\n\n**Fix**: Use custom NavigationClient:\n```typescript\nimport { NavigationClient } from \"@azure\u002Fmsal-browser\";\nimport { useNavigate } from \"react-router-dom\";\n\nclass CustomNavigationClient extends NavigationClient {\n  private navigate: ReturnType\u003Ctypeof useNavigate>;\n\n  constructor(navigate: ReturnType\u003Ctypeof useNavigate>) {\n    super();\n    this.navigate = navigate;\n  }\n\n  async navigateInternal(url: string, options: { noHistory: boolean }) {\n    const relativePath = url.replace(window.location.origin, \"\");\n    if (options.noHistory) {\n      this.navigate(relativePath, { replace: true });\n    } else {\n      this.navigate(relativePath);\n    }\n    return false; \u002F\u002F Prevent MSAL from doing its own navigation\n  }\n}\n\n\u002F\u002F In your App component:\nconst navigate = useNavigate();\nuseEffect(() => {\n  const navigationClient = new CustomNavigationClient(navigate);\n  instance.setNavigationClient(navigationClient);\n}, [instance, navigate]);\n```\n\n### 4. NextJS Dynamic Route Error\n\n**Error**: `no_cached_authority_error` in dynamic routes.\n\n**Cause**: MSAL instance not properly initialized before component renders.\n\n**Fix**: Initialize MSAL in `_app.tsx` before any routing:\n```typescript\n\u002F\u002F pages\u002F_app.tsx\nimport { PublicClientApplication } from \"@azure\u002Fmsal-browser\";\nimport { MsalProvider } from \"@azure\u002Fmsal-react\";\nimport { msalConfig } from \"..\u002Fauth\u002Fmsal-config\";\n\n\u002F\u002F Initialize outside component\nconst msalInstance = new PublicClientApplication(msalConfig);\n\n\u002F\u002F Ensure initialization completes before render\nexport default function App({ Component, pageProps }) {\n  const [isInitialized, setIsInitialized] = useState(false);\n\n  useEffect(() => {\n    msalInstance.initialize().then(() => setIsInitialized(true));\n  }, []);\n\n  if (!isInitialized) return \u003Cdiv>Loading...\u003C\u002Fdiv>;\n\n  return (\n    \u003CMsalProvider instance={msalInstance}>\n      \u003CComponent {...pageProps} \u002F>\n    \u003C\u002FMsalProvider>\n  );\n}\n```\n\n### 5. Safari\u002FEdge Cookie Issues\n\n**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.\n\n**Source**: [GitHub Issue #7384](https:\u002F\u002Fgithub.com\u002FAzureAD\u002Fmicrosoft-authentication-library-for-js\u002Fissues\u002F7384)\n\n**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.\n\n**Testing Note**: Works in Chrome on iOS 18, but fails in Safari on iOS 18.\n\n**Fix**: Enable cookie storage in MSAL config:\n```typescript\ncache: {\n  cacheLocation: \"localStorage\",\n  storeAuthStateInCookie: true, \u002F\u002F REQUIRED for Safari\u002FEdge\n}\n```\n\n**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.\n\n### 6. JWKS URL Not Found (Workers)\n\n**Error**: Failed to fetch JWKS from `.well-known\u002Fjwks.json`.\n\n**Cause**: Azure AD doesn't serve JWKS at the standard OpenID Connect path.\n\n**Fix**: Fetch `openid-configuration` first, then use `jwks_uri`:\n```typescript\n\u002F\u002F WRONG - Azure AD doesn't use this path\nconst jwks = createRemoteJWKSet(\n  new URL(`https:\u002F\u002Flogin.microsoftonline.com\u002F${tenantId}\u002F.well-known\u002Fjwks.json`)\n);\n\n\u002F\u002F CORRECT - Fetch config first\nconst config = await fetch(\n  `https:\u002F\u002Flogin.microsoftonline.com\u002F${tenantId}\u002Fv2.0\u002F.well-known\u002Fopenid-configuration`\n).then(r => r.json());\nconst jwks = createRemoteJWKSet(new URL(config.jwks_uri));\n```\n\n### 7. React Router Loader State Conflict\n\n**Error**: React warning about updating state during render when using `acquireTokenSilent` in React Router loaders.\n\n**Source**: [GitHub Issue #7068](https:\u002F\u002Fgithub.com\u002FAzureAD\u002Fmicrosoft-authentication-library-for-js\u002Fissues\u002F7068)\n\n**Cause**: Using the same `PublicClientApplication` instance in both the router loader and `MsalProvider` causes state updates during rendering.\n\n**Fix**: Call `initialize()` again in the loader:\n```typescript\nconst protectedLoader = async () => {\n  await msalInstance.initialize(); \u002F\u002F Prevents state conflict\n  const response = await msalInstance.acquireTokenSilent(request);\n  return { data };\n};\n```\n\n### 8. setActiveAccount Doesn't Trigger Re-render (Community-sourced)\n\n**Error**: Components using `useMsal()` don't update after calling `setActiveAccount()`.\n\n**Source**: [GitHub Issue #6989](https:\u002F\u002Fgithub.com\u002FAzureAD\u002Fmicrosoft-authentication-library-for-js\u002Fissues\u002F6989)\n\n**Verified**: Multiple users confirmed in GitHub issue\n\n**Cause**: `setActiveAccount()` updates the MSAL instance but doesn't notify React of the change.\n\n**Fix**: Force re-render with state:\n```typescript\nconst [accountKey, setAccountKey] = useState(0);\n\nconst switchAccount = (newAccount) => {\n  msalInstance.setActiveAccount(newAccount);\n  setAccountKey(prev => prev + 1); \u002F\u002F Force update\n};\n```\n\n---\n\n## Multi-Tenant vs Single-Tenant\n\n### Single Tenant (Recommended for Enterprise)\n\n```typescript\nauthority: `https:\u002F\u002Flogin.microsoftonline.com\u002F${TENANT_ID}`,\n```\n\n- Only users from your organization can sign in\n- Token issuer: `https:\u002F\u002Flogin.microsoftonline.com\u002F{tenant_id}\u002Fv2.0`\n\n### Multi-Tenant\n\n```typescript\nauthority: \"https:\u002F\u002Flogin.microsoftonline.com\u002Fcommon\",\n\u002F\u002F or for work\u002Fschool accounts only:\nauthority: \"https:\u002F\u002Flogin.microsoftonline.com\u002Forganizations\",\n```\n\n- Users from any Azure AD tenant can sign in\n- Token issuer varies by user's tenant\n- **Backend validation must handle multiple issuers**:\n\n```typescript\n\u002F\u002F Multi-tenant issuer validation\nconst tenantId = payload.tid; \u002F\u002F Tenant ID from token\nconst expectedIssuer = `https:\u002F\u002Flogin.microsoftonline.com\u002F${tenantId}\u002Fv2.0`;\nif (payload.iss !== expectedIssuer) {\n  throw new Error(\"Invalid issuer\");\n}\n```\n\n---\n\n## Environment Variables\n\n### Frontend (.env)\n\n```bash\nVITE_AZURE_CLIENT_ID=your-client-id-guid\nVITE_AZURE_TENANT_ID=your-tenant-id-guid\n```\n\n### Backend (wrangler.jsonc)\n\n```jsonc\n{\n  \"name\": \"my-api\",\n  \"vars\": {\n    \"AZURE_TENANT_ID\": \"your-tenant-id-guid\",\n    \"AZURE_CLIENT_ID\": \"your-client-id-guid\"\n  }\n}\n```\n\n---\n\n## Azure AD B2C Sunset\n\n**Timeline**:\n- **May 1, 2025**: Azure AD B2C no longer available for new customer signups\n- **March 15, 2026**: Azure AD B2C P2 discontinued for all customers\n- **May 2030**: Microsoft will continue supporting existing B2C customers with standard support\n\n**Source**: [Microsoft Q&A](https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fanswers\u002Fquestions\u002F2119363\u002Fmigrating-existing-azure-ad-b2c-to-microsoft-entra)\n\n**Existing B2C Customers**: Can continue using B2C until 2030, but should plan migration to Entra External ID.\n\n**New Projects**: Use **Microsoft Entra External ID** for consumer\u002Fcustomer identity scenarios.\n\n**Migration Status**: As of January 2026, automated migration tools are in testing phase. Manual migration guidance available at Microsoft Learn.\n\n**Migration Path**:\n- Different authority URL format (`{tenant}.ciamlogin.com` vs `{tenant}.b2clogin.com`)\n- Updated SDK support (same MSAL libraries)\n- New pricing model (consumption-based)\n- Self-Service Password Reset (SSPR) approach available for user migration\n- Seamless migration samples on GitHub (preview)\n\nSee: https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fentra\u002Fexternal-id\u002F\nMigration Guide: https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fentra\u002Fexternal-id\u002Fcustomers\u002Fhow-to-migrate-users\n\n---\n\n## ADAL Retirement (Complete)\n\n**Status**: Azure AD Authentication Library (ADAL) was retired on September 30, 2025. Apps using ADAL no longer receive security updates.\n\n**If you're migrating from ADAL**:\n1. ADAL → MSAL migration is required\n2. ADAL used v1.0 endpoints; MSAL uses v2.0 endpoints\n3. Token cache format differs - users must re-authenticate\n4. Scopes replace \"resources\" in token requests\n\n**Key Migration Changes**:\n```typescript\n\u002F\u002F ADAL (deprecated) - resource-based\nacquireToken({ resource: \"https:\u002F\u002Fgraph.microsoft.com\" })\n\n\u002F\u002F MSAL (current) - scope-based\nacquireTokenSilent({ scopes: [\"https:\u002F\u002Fgraph.microsoft.com\u002FUser.Read\"] })\n```\n\nSee: https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fentra\u002Fmsal\u002Fjavascript\u002Fmigration\u002Fmsal-net-migration\n\n---\n\n## Resources\n\n- [MSAL React Documentation](https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fentra\u002Fmsal\u002Fjavascript\u002Freact\u002F)\n- [Microsoft Entra ID App Registration](https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fentra\u002Fidentity-platform\u002Fquickstart-register-app)\n- [MSAL.js GitHub Issues](https:\u002F\u002Fgithub.com\u002FAzureAD\u002Fmicrosoft-authentication-library-for-js\u002Fissues)\n- [jose Library](https:\u002F\u002Fgithub.com\u002Fpanva\u002Fjose)\n- [Cloudflare Workers + Azure AD Blog](https:\u002F\u002Fhajekj.net\u002F2021\u002F11\u002F12\u002Fcloudflare-workers-and-azure-ad\u002F)\n",{"name":5,"description":51,"user-invocable":52},"Microsoft Entra ID (Azure AD) authentication for React SPAs with MSAL.js and Cloudflare Workers JWT validation using jose library. Full-stack pattern with Authorization Code Flow + PKCE. Prevents 8 documented errors.\n\nUse when: implementing Microsoft SSO, troubleshooting AADSTS50058 loops, AADSTS700084 refresh token errors, React Router redirects, setActiveAccount re-render issues, or validating Entra ID tokens in Workers.\n",true,"main","e7e336ac628c3e3bd771aff1aaa469c93778f94a","skills\u002Fveeramanikandanr48\u002Fazure-auth",0,"https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del","2026-08-23 22:39:37",{"source":8,"skillName":5,"commands":61,"prompt":71,"zipAvailable":52},[62,65,68],{"tool":63,"command":64},"npx","npx skills add kbarbel640-del\u002Fskills --skill azure-auth",{"tool":66,"command":67},"bunx","bunx skills add kbarbel640-del\u002Fskills --skill azure-auth",{"tool":69,"command":70},"pnpm","pnpm dlx skills add kbarbel640-del\u002Fskills --skill azure-auth","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fveeramanikandanr48\u002Fazure-auth\u002FSKILL.md",[73,79,83,87,91,95,101,107,111,115],{"path":74,"resourceType":75,"resourceTypeDesc":76,"sizeBytes":77,"sourceHtmlUrl":78},"_meta.json","REFERENCE","参考文档",284,"https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fveeramanikandanr48\u002Fazure-auth\u002F_meta.json",{"path":80,"resourceType":75,"resourceTypeDesc":76,"sizeBytes":81,"sourceHtmlUrl":82},".claude-plugin\u002Fplugin.json",688,"https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fveeramanikandanr48\u002Fazure-auth\u002F.claude-plugin\u002Fplugin.json",{"path":84,"resourceType":75,"resourceTypeDesc":76,"sizeBytes":85,"sourceHtmlUrl":86},"README.md",2600,"https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fveeramanikandanr48\u002Fazure-auth\u002FREADME.md",{"path":88,"resourceType":75,"resourceTypeDesc":76,"sizeBytes":89,"sourceHtmlUrl":90},"references\u002Faadsts-error-codes.md",7886,"https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fveeramanikandanr48\u002Fazure-auth\u002Freferences\u002Faadsts-error-codes.md",{"path":92,"resourceType":75,"resourceTypeDesc":76,"sizeBytes":93,"sourceHtmlUrl":94},"rules\u002Fazure-auth.md",3509,"https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fveeramanikandanr48\u002Fazure-auth\u002Frules\u002Fazure-auth.md",{"path":96,"resourceType":97,"resourceTypeDesc":98,"sizeBytes":99,"sourceHtmlUrl":100},"templates\u002Fmsal-config.ts","SCRIPT","脚本",3008,"https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fveeramanikandanr48\u002Fazure-auth\u002Ftemplates\u002Fmsal-config.ts",{"path":102,"resourceType":103,"resourceTypeDesc":104,"sizeBytes":105,"sourceHtmlUrl":106},"templates\u002Fmsal-provider.tsx","OTHER","其他",4575,"https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fveeramanikandanr48\u002Fazure-auth\u002Ftemplates\u002Fmsal-provider.tsx",{"path":108,"resourceType":103,"resourceTypeDesc":104,"sizeBytes":109,"sourceHtmlUrl":110},"templates\u002Fprotected-route.tsx",7309,"https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fveeramanikandanr48\u002Fazure-auth\u002Ftemplates\u002Fprotected-route.tsx",{"path":112,"resourceType":97,"resourceTypeDesc":98,"sizeBytes":113,"sourceHtmlUrl":114},"templates\u002Fuse-api-token.ts",6171,"https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fveeramanikandanr48\u002Fazure-auth\u002Ftemplates\u002Fuse-api-token.ts",{"path":116,"resourceType":97,"resourceTypeDesc":98,"sizeBytes":117,"sourceHtmlUrl":118},"templates\u002Fworkers-jwt-validation.ts",8549,"https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fveeramanikandanr48\u002Fazure-auth\u002Ftemplates\u002Fworkers-jwt-validation.ts",[120,151,178,199,205,225],{"skillNo":121,"name":122,"nameZh":6,"description":123,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":124,"categories":125,"keywords":131,"publishedAt":46},"01M6G8J55967D6281ACMKAKTEQ","crowd-prompting","A marketplace where AI agents improve prompts, system instructions, tool descriptions, and other text-based content with domain expertise from real-world operations — and earn tokens for valuable contributions.","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fzzadrian\u002Fcrowd-prompting\u002FSKILL.md",[126],{"categoryNo":127,"categoryName":128,"taxonomyType":129,"taxonomyTypeDesc":130},"01JBSEEDCAT000000000000036","内容创作者","PROFESSION","适用职业",[132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150],"crowd prompting","prompting","api","with","tokens","domain","world","prompts","operations","instructions","text","tool","system","improve","productivity","marketplace","https","content","ai agents",{"skillNo":152,"name":153,"nameZh":6,"description":154,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":155,"categories":156,"keywords":160,"publishedAt":46},"01M6G8J0PKPAYVET6BWE8W4C26","dilemm-ai","Compete in DilemmAI, the prisoner's dilemma AI arena at dilemm.ai. Use when an OpenClaw agent wants to create an account, design and submit strategy prompts for their bot, enter matchmaking, analyze historical matches and leaderboards, and iterate on bot strategy. Requires headless browser automation (no API available yet).","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fzyori\u002Fdilemm-ai\u002FSKILL.md",[157],{"categoryNo":158,"categoryName":159,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000023","Skill与智能体开发",[161,134,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,139,177],"dilemm ai","arena","analyze","browser","openclaw agent","create","automation","browser automation","bot","openclaw","design","strategy","headless","compete","submit","matchmaking","use",{"skillNo":179,"name":180,"nameZh":6,"description":181,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":182,"categories":183,"keywords":187,"publishedAt":46},"01M6G8HWAF3K9SW7NP3Z83CPJ6","arxiv-paper-reviews","Interact with arXiv Crawler API to fetch papers, read reviews, and submit comments. Use when working with arXiv papers, fetching paper lists by date\u002Fcategory\u002Finterest, viewing paper details with comments, or submitting paper reviews via API at http:\u002F\u002F150.158.152.82:8000.","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fzxrys\u002Fweak-accept\u002FSKILL.md",[184],{"categoryNo":185,"categoryName":186,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000021","资料检索与研究",[188,189,190,191,192,193,194,135,195,175,177,196,134,197,198],"arxiv paper reviews","reviews","arxiv","paper","crawler","papers","comments","fetch","read","http","via",{"skillNo":200,"name":180,"nameZh":6,"description":181,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":201,"categories":202,"keywords":204,"publishedAt":46},"01M6G8HQJ7N7WMY6YV7D7SSJ16","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fzxrys\u002Farxiv-paper-reviews\u002FSKILL.md",[203],{"categoryNo":185,"categoryName":186,"taxonomyType":20,"taxonomyTypeDesc":21},[191,190,189,188,198,193,194,135,192,175,177,195,196,134,197],{"skillNo":206,"name":207,"nameZh":6,"description":208,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":209,"categories":210,"keywords":211,"publishedAt":46},"01M6G8HM95Y64JNVFC2HN4E2BG","git-summary","Get a quick summary of the current Git repository including status, recent commits, branches, and contributors.","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fzweack\u002Fgit-summary\u002FSKILL.md",[],[212,213,214,215,216,217,218,219,220,221,222,223,224],"git","summary","git summary","commits","repository","win32","status","linux","contributors","true","quick","get","branches",{"skillNo":226,"name":227,"nameZh":6,"description":228,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":229,"categories":230,"keywords":235,"publishedAt":46},"01M6G8HGEGF05CNEJCGVYFSC4R","agent-constitution","Interact with AgentConstitution governance contracts on Base Sepolia. Check compliance, read rules, log actions, query governance state.","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fztsalexey\u002Fagent-constitution\u002FSKILL.md",[231,232],{"categoryNo":158,"categoryName":159,"taxonomyType":20,"taxonomyTypeDesc":21},{"categoryNo":233,"categoryName":234,"taxonomyType":129,"taxonomyTypeDesc":130},"01JBSEEDCAT000000000000037","律师与法务",[236,237,238,239,135,240,241,53,242,196,243,244,245,246,247,148,248,249,250],"agent constitution","constitution","contracts","tree","query","base","log","check","Rules","projects","compliance","actions","state","governance","github"]