{
  "schemaVersion": "1.0",
  "item": {
    "slug": "azure-auth",
    "name": "Azure Auth",
    "source": "tencent",
    "type": "skill",
    "category": "开发工具",
    "sourceUrl": "https://clawhub.ai/Veeramanikandanr48/azure-auth",
    "canonicalUrl": "https://clawhub.ai/Veeramanikandanr48/azure-auth",
    "targetPlatform": "OpenClaw"
  },
  "install": {
    "downloadMode": "redirect",
    "downloadUrl": "/downloads/azure-auth",
    "sourceDownloadUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=azure-auth",
    "sourcePlatform": "tencent",
    "targetPlatform": "OpenClaw",
    "installMethod": "Manual import",
    "extraction": "Extract archive",
    "prerequisites": [
      "OpenClaw"
    ],
    "packageFormat": "ZIP package",
    "includedAssets": [
      ".claude-plugin/plugin.json",
      "README.md",
      "SKILL.md",
      "references/aadsts-error-codes.md",
      "rules/azure-auth.md",
      "templates/msal-config.ts"
    ],
    "primaryDoc": "SKILL.md",
    "quickSetup": [
      "Download the package from Yavira.",
      "Extract the archive and review SKILL.md first.",
      "Import or place the package into your OpenClaw setup."
    ],
    "agentAssist": {
      "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
      "steps": [
        "Download the package from Yavira.",
        "Extract it into a folder your agent can access.",
        "Paste one of the prompts below and point your agent at the extracted folder."
      ],
      "prompts": [
        {
          "label": "New install",
          "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Then review README.md for any prerequisites, environment setup, or post-install checks. Tell me what you changed and call out any manual steps you could not complete."
        },
        {
          "label": "Upgrade existing",
          "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Then review README.md for any prerequisites, environment setup, or post-install checks. Summarize what changed and any follow-up checks I should run."
        }
      ]
    },
    "sourceHealth": {
      "source": "tencent",
      "status": "healthy",
      "reason": "direct_download_ok",
      "recommendedAction": "download",
      "checkedAt": "2026-04-23T16:43:11.935Z",
      "expiresAt": "2026-04-30T16:43:11.935Z",
      "httpStatus": 200,
      "finalUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=4claw-imageboard",
      "contentType": "application/zip",
      "probeMethod": "head",
      "details": {
        "probeUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=4claw-imageboard",
        "contentDisposition": "attachment; filename=\"4claw-imageboard-1.0.1.zip\"",
        "redirectLocation": null,
        "bodySnippet": null
      },
      "scope": "source",
      "summary": "Source download looks usable.",
      "detail": "Yavira can redirect you to the upstream package for this source.",
      "primaryActionLabel": "Download for OpenClaw",
      "primaryActionHref": "/downloads/azure-auth"
    },
    "validation": {
      "installChecklist": [
        "Use the Yavira download entry.",
        "Review SKILL.md after the package is downloaded.",
        "Confirm the extracted package contains the expected setup assets."
      ],
      "postInstallChecks": [
        "Confirm the extracted package includes the expected docs or setup files.",
        "Validate the skill or prompts are available in your target agent workspace.",
        "Capture any manual follow-up steps the agent could not complete."
      ]
    },
    "downloadPageUrl": "https://openagent3.xyz/downloads/azure-auth",
    "agentPageUrl": "https://openagent3.xyz/skills/azure-auth/agent",
    "manifestUrl": "https://openagent3.xyz/skills/azure-auth/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/azure-auth/agent.md"
  },
  "agentAssist": {
    "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
    "steps": [
      "Download the package from Yavira.",
      "Extract it into a folder your agent can access.",
      "Paste one of the prompts below and point your agent at the extracted folder."
    ],
    "prompts": [
      {
        "label": "New install",
        "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Then review README.md for any prerequisites, environment setup, or post-install checks. Tell me what you changed and call out any manual steps you could not complete."
      },
      {
        "label": "Upgrade existing",
        "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Then review README.md for any prerequisites, environment setup, or post-install checks. Summarize what changed and any follow-up checks I should run."
      }
    ]
  },
  "documentation": {
    "source": "clawhub",
    "primaryDoc": "SKILL.md",
    "sections": [
      {
        "title": "Azure Auth - Microsoft Entra ID for React + Cloudflare Workers",
        "body": "Package Versions: @azure/msal-react@5.0.2, @azure/msal-browser@5.0.2, jose@6.1.3\nBreaking Changes: MSAL v4→v5 migration (January 2026), Azure AD B2C sunset (May 2025 - new signups blocked, existing until 2030), ADAL retirement (Sept 2025 - complete)\nLast Updated: 2026-01-21"
      },
      {
        "title": "Architecture Overview",
        "body": "┌─────────────────────┐     ┌──────────────────────┐     ┌─────────────────────┐\n│   React SPA         │────▶│  Microsoft Entra ID  │────▶│  Cloudflare Worker  │\n│   @azure/msal-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\nKey Constraint: MSAL.js does NOT work in Cloudflare Workers (relies on browser/Node.js APIs). Use jose library for backend token validation."
      },
      {
        "title": "1. Install Dependencies",
        "body": "# Frontend (React SPA)\nnpm install @azure/msal-react @azure/msal-browser\n\n# Backend (Cloudflare Workers)\nnpm install jose"
      },
      {
        "title": "2. Azure Portal Setup",
        "body": "Go to Microsoft Entra ID → App registrations → New registration\nSet Redirect URI to http://localhost:5173 (SPA type)\nNote the Application (client) ID and Directory (tenant) ID\nUnder Authentication:\n\nEnable Access tokens and ID tokens\nAdd production redirect URI\n\n\nUnder API permissions:\n\nAdd User.Read (Microsoft Graph)\nGrant admin consent if required"
      },
      {
        "title": "Configuration (src/auth/msal-config.ts)",
        "body": "import { Configuration, LogLevel } from \"@azure/msal-browser\";\n\nexport const msalConfig: Configuration = {\n  auth: {\n    clientId: import.meta.env.VITE_AZURE_CLIENT_ID,\n    authority: `https://login.microsoftonline.com/${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\", // or \"sessionStorage\"\n    storeAuthStateInCookie: true, // Required for Safari/Edge 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// Scopes for token requests\nexport const loginRequest = {\n  scopes: [\"User.Read\", \"openid\", \"profile\", \"email\"],\n};\n\n// Scopes for API calls (add your API scope here)\nexport const apiRequest = {\n  scopes: [`api://${import.meta.env.VITE_AZURE_CLIENT_ID}/access_as_user`],\n};"
      },
      {
        "title": "MsalProvider Setup (src/main.tsx)",
        "body": "import React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport { PublicClientApplication, EventType } from \"@azure/msal-browser\";\nimport { MsalProvider } from \"@azure/msal-react\";\nimport { msalConfig } from \"./auth/msal-config\";\nimport App from \"./App\";\n\n// CRITICAL: Initialize MSAL outside component tree to prevent re-instantiation\nconst msalInstance = new PublicClientApplication(msalConfig);\n\n// Handle redirect promise on page load\nmsalInstance.initialize().then(() => {\n  // Set active account after redirect\n  // 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  // 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    <React.StrictMode>\n      <MsalProvider instance={msalInstance}>\n        <App />\n      </MsalProvider>\n    </React.StrictMode>\n  );\n});"
      },
      {
        "title": "Protected Route Component",
        "body": "import { useMsal, useIsAuthenticated } from \"@azure/msal-react\";\nimport { InteractionStatus } from \"@azure/msal-browser\";\nimport { loginRequest } from \"./msal-config\";\n\nexport function ProtectedRoute({ children }: { children: React.ReactNode }) {\n  const { instance, inProgress } = useMsal();\n  const isAuthenticated = useIsAuthenticated();\n\n  // Wait for MSAL to finish any in-progress operations\n  if (inProgress !== InteractionStatus.None) {\n    return <div>Loading...</div>;\n  }\n\n  if (!isAuthenticated) {\n    // Trigger login redirect\n    instance.loginRedirect(loginRequest);\n    return <div>Redirecting to login...</div>;\n  }\n\n  return <>{children}</>;\n}"
      },
      {
        "title": "Acquiring Tokens for API Calls",
        "body": "import { useMsal } from \"@azure/msal-react\";\nimport { InteractionRequiredAuthError } from \"@azure/msal-browser\";\nimport { apiRequest } from \"./msal-config\";\n\nexport function useApiToken() {\n  const { instance, accounts } = useMsal();\n\n  async function getAccessToken(): Promise<string | null> {\n    if (accounts.length === 0) return null;\n\n    const request = {\n      ...apiRequest,\n      account: accounts[0],\n    };\n\n    try {\n      // 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        // Silent acquisition failed, need interactive login\n        // 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}"
      },
      {
        "title": "Why jose Instead of MSAL",
        "body": "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."
      },
      {
        "title": "JWT Validation (src/auth/validate-token.ts)",
        "body": "import * as jose from \"jose\";\n\ninterface EntraTokenPayload {\n  aud: string;       // Audience (your client ID or API URI)\n  iss: string;       // Issuer (https://login.microsoftonline.com/{tenant}/v2.0)\n  sub: string;       // Subject (user's unique ID)\n  oid: string;       // Object ID (user's Azure AD object ID)\n  preferred_username: string;\n  name: string;\n  email?: string;\n  roles?: string[];  // App roles if configured\n  scp?: string;      // Scopes (space-separated)\n}\n\n// Cache JWKS to avoid fetching on every request\nlet jwksCache: jose.JWTVerifyGetKey | null = null;\nlet jwksCacheTime = 0;\nconst JWKS_CACHE_DURATION = 3600000; // 1 hour\n\nasync function getJWKS(tenantId: string): Promise<jose.JWTVerifyGetKey> {\n  const now = Date.now();\n\n  if (jwksCache && now - jwksCacheTime < JWKS_CACHE_DURATION) {\n    return jwksCache;\n  }\n\n  // CRITICAL: Azure AD JWKS is NOT at .well-known/jwks.json\n  // Must fetch from openid-configuration first\n  const configUrl = `https://login.microsoftonline.com/${tenantId}/v2.0/.well-known/openid-configuration`;\n  const configResponse = await fetch(configUrl);\n  const config = await configResponse.json() as { jwks_uri: string };\n\n  // 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<EntraTokenPayload | null> {\n  try {\n    const jwks = await getJWKS(env.AZURE_TENANT_ID);\n\n    const { payload } = await jose.jwtVerify(token, jwks, {\n      issuer: `https://login.microsoftonline.com/${env.AZURE_TENANT_ID}/v2.0`,\n      audience: env.AZURE_CLIENT_ID, // or your API URI: api://{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}"
      },
      {
        "title": "Worker Middleware Pattern",
        "body": "import { validateEntraToken } from \"./auth/validate-token\";\n\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    // Skip auth for public routes\n    const url = new URL(request.url);\n    if (url.pathname === \"/\" || url.pathname.startsWith(\"/public\")) {\n      return handlePublicRoute(request, env);\n    }\n\n    // 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/json\" },\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/json\" },\n      });\n    }\n\n    // Add user to request context\n    const requestWithUser = new Request(request);\n    // Pass user info downstream (e.g., via headers or context)\n\n    return handleProtectedRoute(request, env, user);\n  },\n};"
      },
      {
        "title": "1. AADSTS50058 - Silent Sign-In Loop",
        "body": "Error: \"A silent sign-in request was sent but no user is signed in\"\n\nCause: acquireTokenSilent called when no cached user exists.\n\nFix:\n\n// Always check for accounts before silent acquisition\nconst accounts = instance.getAllAccounts();\nif (accounts.length === 0) {\n  // No cached user, trigger interactive login\n  await instance.loginRedirect(loginRequest);\n  return;\n}"
      },
      {
        "title": "2. AADSTS700084 - Refresh Token Expired",
        "body": "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\nCause: SPA refresh tokens expire after 24 hours. Cannot be extended.\n\nFix:\n\ntry {\n  const response = await instance.acquireTokenSilent(request);\n} catch (error) {\n  if (error instanceof InteractionRequiredAuthError) {\n    // Refresh token expired, need fresh login\n    await instance.acquireTokenRedirect(request);\n  }\n}"
      },
      {
        "title": "3. React Router v6 Redirect Loop",
        "body": "Error: Infinite redirects between login page and app.\n\nCause: React Router v6 may strip the hash fragment containing auth response.\n\nFix: Use custom NavigationClient:\n\nimport { NavigationClient } from \"@azure/msal-browser\";\nimport { useNavigate } from \"react-router-dom\";\n\nclass CustomNavigationClient extends NavigationClient {\n  private navigate: ReturnType<typeof useNavigate>;\n\n  constructor(navigate: ReturnType<typeof 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; // Prevent MSAL from doing its own navigation\n  }\n}\n\n// In your App component:\nconst navigate = useNavigate();\nuseEffect(() => {\n  const navigationClient = new CustomNavigationClient(navigate);\n  instance.setNavigationClient(navigationClient);\n}, [instance, navigate]);"
      },
      {
        "title": "4. NextJS Dynamic Route Error",
        "body": "Error: no_cached_authority_error in dynamic routes.\n\nCause: MSAL instance not properly initialized before component renders.\n\nFix: Initialize MSAL in _app.tsx before any routing:\n\n// pages/_app.tsx\nimport { PublicClientApplication } from \"@azure/msal-browser\";\nimport { MsalProvider } from \"@azure/msal-react\";\nimport { msalConfig } from \"../auth/msal-config\";\n\n// Initialize outside component\nconst msalInstance = new PublicClientApplication(msalConfig);\n\n// 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 <div>Loading...</div>;\n\n  return (\n    <MsalProvider instance={msalInstance}>\n      <Component {...pageProps} />\n    </MsalProvider>\n  );\n}"
      },
      {
        "title": "5. Safari/Edge Cookie Issues",
        "body": "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\nSource: GitHub Issue #7384\n\nCause: 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\nTesting Note: Works in Chrome on iOS 18, but fails in Safari on iOS 18.\n\nFix: Enable cookie storage in MSAL config:\n\ncache: {\n  cacheLocation: \"localStorage\",\n  storeAuthStateInCookie: true, // REQUIRED for Safari/Edge\n}\n\niOS 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."
      },
      {
        "title": "6. JWKS URL Not Found (Workers)",
        "body": "Error: Failed to fetch JWKS from .well-known/jwks.json.\n\nCause: Azure AD doesn't serve JWKS at the standard OpenID Connect path.\n\nFix: Fetch openid-configuration first, then use jwks_uri:\n\n// WRONG - Azure AD doesn't use this path\nconst jwks = createRemoteJWKSet(\n  new URL(`https://login.microsoftonline.com/${tenantId}/.well-known/jwks.json`)\n);\n\n// CORRECT - Fetch config first\nconst config = await fetch(\n  `https://login.microsoftonline.com/${tenantId}/v2.0/.well-known/openid-configuration`\n).then(r => r.json());\nconst jwks = createRemoteJWKSet(new URL(config.jwks_uri));"
      },
      {
        "title": "7. React Router Loader State Conflict",
        "body": "Error: React warning about updating state during render when using acquireTokenSilent in React Router loaders.\n\nSource: GitHub Issue #7068\n\nCause: Using the same PublicClientApplication instance in both the router loader and MsalProvider causes state updates during rendering.\n\nFix: Call initialize() again in the loader:\n\nconst protectedLoader = async () => {\n  await msalInstance.initialize(); // Prevents state conflict\n  const response = await msalInstance.acquireTokenSilent(request);\n  return { data };\n};"
      },
      {
        "title": "8. setActiveAccount Doesn't Trigger Re-render (Community-sourced)",
        "body": "Error: Components using useMsal() don't update after calling setActiveAccount().\n\nSource: GitHub Issue #6989\n\nVerified: Multiple users confirmed in GitHub issue\n\nCause: setActiveAccount() updates the MSAL instance but doesn't notify React of the change.\n\nFix: Force re-render with state:\n\nconst [accountKey, setAccountKey] = useState(0);\n\nconst switchAccount = (newAccount) => {\n  msalInstance.setActiveAccount(newAccount);\n  setAccountKey(prev => prev + 1); // Force update\n};"
      },
      {
        "title": "Single Tenant (Recommended for Enterprise)",
        "body": "authority: `https://login.microsoftonline.com/${TENANT_ID}`,\n\nOnly users from your organization can sign in\nToken issuer: https://login.microsoftonline.com/{tenant_id}/v2.0"
      },
      {
        "title": "Multi-Tenant",
        "body": "authority: \"https://login.microsoftonline.com/common\",\n// or for work/school accounts only:\nauthority: \"https://login.microsoftonline.com/organizations\",\n\nUsers from any Azure AD tenant can sign in\nToken issuer varies by user's tenant\nBackend validation must handle multiple issuers:\n\n// Multi-tenant issuer validation\nconst tenantId = payload.tid; // Tenant ID from token\nconst expectedIssuer = `https://login.microsoftonline.com/${tenantId}/v2.0`;\nif (payload.iss !== expectedIssuer) {\n  throw new Error(\"Invalid issuer\");\n}"
      },
      {
        "title": "Frontend (.env)",
        "body": "VITE_AZURE_CLIENT_ID=your-client-id-guid\nVITE_AZURE_TENANT_ID=your-tenant-id-guid"
      },
      {
        "title": "Backend (wrangler.jsonc)",
        "body": "{\n  \"name\": \"my-api\",\n  \"vars\": {\n    \"AZURE_TENANT_ID\": \"your-tenant-id-guid\",\n    \"AZURE_CLIENT_ID\": \"your-client-id-guid\"\n  }\n}"
      },
      {
        "title": "Azure AD B2C Sunset",
        "body": "Timeline:\n\nMay 1, 2025: Azure AD B2C no longer available for new customer signups\nMarch 15, 2026: Azure AD B2C P2 discontinued for all customers\nMay 2030: Microsoft will continue supporting existing B2C customers with standard support\n\nSource: Microsoft Q&A\n\nExisting B2C Customers: Can continue using B2C until 2030, but should plan migration to Entra External ID.\n\nNew Projects: Use Microsoft Entra External ID for consumer/customer identity scenarios.\n\nMigration Status: As of January 2026, automated migration tools are in testing phase. Manual migration guidance available at Microsoft Learn.\n\nMigration Path:\n\nDifferent authority URL format ({tenant}.ciamlogin.com vs {tenant}.b2clogin.com)\nUpdated SDK support (same MSAL libraries)\nNew pricing model (consumption-based)\nSelf-Service Password Reset (SSPR) approach available for user migration\nSeamless migration samples on GitHub (preview)\n\nSee: https://learn.microsoft.com/en-us/entra/external-id/\nMigration Guide: https://learn.microsoft.com/en-us/entra/external-id/customers/how-to-migrate-users"
      },
      {
        "title": "ADAL Retirement (Complete)",
        "body": "Status: Azure AD Authentication Library (ADAL) was retired on September 30, 2025. Apps using ADAL no longer receive security updates.\n\nIf you're migrating from ADAL:\n\nADAL → MSAL migration is required\nADAL used v1.0 endpoints; MSAL uses v2.0 endpoints\nToken cache format differs - users must re-authenticate\nScopes replace \"resources\" in token requests\n\nKey Migration Changes:\n\n// ADAL (deprecated) - resource-based\nacquireToken({ resource: \"https://graph.microsoft.com\" })\n\n// MSAL (current) - scope-based\nacquireTokenSilent({ scopes: [\"https://graph.microsoft.com/User.Read\"] })\n\nSee: https://learn.microsoft.com/en-us/entra/msal/javascript/migration/msal-net-migration"
      },
      {
        "title": "Resources",
        "body": "MSAL React Documentation\nMicrosoft Entra ID App Registration\nMSAL.js GitHub Issues\njose Library\nCloudflare Workers + Azure AD Blog"
      }
    ],
    "body": "Azure Auth - Microsoft Entra ID for React + Cloudflare Workers\n\nPackage 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\n\nArchitecture Overview\n┌─────────────────────┐     ┌──────────────────────┐     ┌─────────────────────┐\n│   React SPA         │────▶│  Microsoft Entra ID  │────▶│  Cloudflare Worker  │\n│   @azure/msal-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\nKey Constraint: MSAL.js does NOT work in Cloudflare Workers (relies on browser/Node.js APIs). Use jose library for backend token validation.\n\nQuick Start\n1. Install Dependencies\n# Frontend (React SPA)\nnpm install @azure/msal-react @azure/msal-browser\n\n# Backend (Cloudflare Workers)\nnpm install jose\n\n2. Azure Portal Setup\nGo to Microsoft Entra ID → App registrations → New registration\nSet Redirect URI to http://localhost:5173 (SPA type)\nNote the Application (client) ID and Directory (tenant) ID\nUnder Authentication:\nEnable Access tokens and ID tokens\nAdd production redirect URI\nUnder API permissions:\nAdd User.Read (Microsoft Graph)\nGrant admin consent if required\nFrontend: MSAL React Setup\nConfiguration (src/auth/msal-config.ts)\nimport { Configuration, LogLevel } from \"@azure/msal-browser\";\n\nexport const msalConfig: Configuration = {\n  auth: {\n    clientId: import.meta.env.VITE_AZURE_CLIENT_ID,\n    authority: `https://login.microsoftonline.com/${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\", // or \"sessionStorage\"\n    storeAuthStateInCookie: true, // Required for Safari/Edge 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// Scopes for token requests\nexport const loginRequest = {\n  scopes: [\"User.Read\", \"openid\", \"profile\", \"email\"],\n};\n\n// Scopes for API calls (add your API scope here)\nexport const apiRequest = {\n  scopes: [`api://${import.meta.env.VITE_AZURE_CLIENT_ID}/access_as_user`],\n};\n\nMsalProvider Setup (src/main.tsx)\nimport React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport { PublicClientApplication, EventType } from \"@azure/msal-browser\";\nimport { MsalProvider } from \"@azure/msal-react\";\nimport { msalConfig } from \"./auth/msal-config\";\nimport App from \"./App\";\n\n// CRITICAL: Initialize MSAL outside component tree to prevent re-instantiation\nconst msalInstance = new PublicClientApplication(msalConfig);\n\n// Handle redirect promise on page load\nmsalInstance.initialize().then(() => {\n  // Set active account after redirect\n  // 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  // 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    <React.StrictMode>\n      <MsalProvider instance={msalInstance}>\n        <App />\n      </MsalProvider>\n    </React.StrictMode>\n  );\n});\n\nProtected Route Component\nimport { useMsal, useIsAuthenticated } from \"@azure/msal-react\";\nimport { InteractionStatus } from \"@azure/msal-browser\";\nimport { loginRequest } from \"./msal-config\";\n\nexport function ProtectedRoute({ children }: { children: React.ReactNode }) {\n  const { instance, inProgress } = useMsal();\n  const isAuthenticated = useIsAuthenticated();\n\n  // Wait for MSAL to finish any in-progress operations\n  if (inProgress !== InteractionStatus.None) {\n    return <div>Loading...</div>;\n  }\n\n  if (!isAuthenticated) {\n    // Trigger login redirect\n    instance.loginRedirect(loginRequest);\n    return <div>Redirecting to login...</div>;\n  }\n\n  return <>{children}</>;\n}\n\nAcquiring Tokens for API Calls\nimport { useMsal } from \"@azure/msal-react\";\nimport { InteractionRequiredAuthError } from \"@azure/msal-browser\";\nimport { apiRequest } from \"./msal-config\";\n\nexport function useApiToken() {\n  const { instance, accounts } = useMsal();\n\n  async function getAccessToken(): Promise<string | null> {\n    if (accounts.length === 0) return null;\n\n    const request = {\n      ...apiRequest,\n      account: accounts[0],\n    };\n\n    try {\n      // 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        // Silent acquisition failed, need interactive login\n        // 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\nBackend: Cloudflare Workers JWT Validation\nWhy 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\nJWT Validation (src/auth/validate-token.ts)\nimport * as jose from \"jose\";\n\ninterface EntraTokenPayload {\n  aud: string;       // Audience (your client ID or API URI)\n  iss: string;       // Issuer (https://login.microsoftonline.com/{tenant}/v2.0)\n  sub: string;       // Subject (user's unique ID)\n  oid: string;       // Object ID (user's Azure AD object ID)\n  preferred_username: string;\n  name: string;\n  email?: string;\n  roles?: string[];  // App roles if configured\n  scp?: string;      // Scopes (space-separated)\n}\n\n// Cache JWKS to avoid fetching on every request\nlet jwksCache: jose.JWTVerifyGetKey | null = null;\nlet jwksCacheTime = 0;\nconst JWKS_CACHE_DURATION = 3600000; // 1 hour\n\nasync function getJWKS(tenantId: string): Promise<jose.JWTVerifyGetKey> {\n  const now = Date.now();\n\n  if (jwksCache && now - jwksCacheTime < JWKS_CACHE_DURATION) {\n    return jwksCache;\n  }\n\n  // CRITICAL: Azure AD JWKS is NOT at .well-known/jwks.json\n  // Must fetch from openid-configuration first\n  const configUrl = `https://login.microsoftonline.com/${tenantId}/v2.0/.well-known/openid-configuration`;\n  const configResponse = await fetch(configUrl);\n  const config = await configResponse.json() as { jwks_uri: string };\n\n  // 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<EntraTokenPayload | null> {\n  try {\n    const jwks = await getJWKS(env.AZURE_TENANT_ID);\n\n    const { payload } = await jose.jwtVerify(token, jwks, {\n      issuer: `https://login.microsoftonline.com/${env.AZURE_TENANT_ID}/v2.0`,\n      audience: env.AZURE_CLIENT_ID, // or your API URI: api://{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\nWorker Middleware Pattern\nimport { validateEntraToken } from \"./auth/validate-token\";\n\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    // Skip auth for public routes\n    const url = new URL(request.url);\n    if (url.pathname === \"/\" || url.pathname.startsWith(\"/public\")) {\n      return handlePublicRoute(request, env);\n    }\n\n    // 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/json\" },\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/json\" },\n      });\n    }\n\n    // Add user to request context\n    const requestWithUser = new Request(request);\n    // Pass user info downstream (e.g., via headers or context)\n\n    return handleProtectedRoute(request, env, user);\n  },\n};\n\nCommon Errors & Fixes\n1. AADSTS50058 - Silent Sign-In Loop\n\nError: \"A silent sign-in request was sent but no user is signed in\"\n\nCause: acquireTokenSilent called when no cached user exists.\n\nFix:\n\n// Always check for accounts before silent acquisition\nconst accounts = instance.getAllAccounts();\nif (accounts.length === 0) {\n  // No cached user, trigger interactive login\n  await instance.loginRedirect(loginRequest);\n  return;\n}\n\n2. AADSTS700084 - Refresh Token Expired\n\nError: \"The refresh token was issued to a single page app (SPA), and therefore has a fixed, limited lifetime of 1.00:00:00\"\n\nCause: SPA refresh tokens expire after 24 hours. Cannot be extended.\n\nFix:\n\ntry {\n  const response = await instance.acquireTokenSilent(request);\n} catch (error) {\n  if (error instanceof InteractionRequiredAuthError) {\n    // Refresh token expired, need fresh login\n    await instance.acquireTokenRedirect(request);\n  }\n}\n\n3. React Router v6 Redirect Loop\n\nError: Infinite redirects between login page and app.\n\nCause: React Router v6 may strip the hash fragment containing auth response.\n\nFix: Use custom NavigationClient:\n\nimport { NavigationClient } from \"@azure/msal-browser\";\nimport { useNavigate } from \"react-router-dom\";\n\nclass CustomNavigationClient extends NavigationClient {\n  private navigate: ReturnType<typeof useNavigate>;\n\n  constructor(navigate: ReturnType<typeof 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; // Prevent MSAL from doing its own navigation\n  }\n}\n\n// In your App component:\nconst navigate = useNavigate();\nuseEffect(() => {\n  const navigationClient = new CustomNavigationClient(navigate);\n  instance.setNavigationClient(navigationClient);\n}, [instance, navigate]);\n\n4. NextJS Dynamic Route Error\n\nError: no_cached_authority_error in dynamic routes.\n\nCause: MSAL instance not properly initialized before component renders.\n\nFix: Initialize MSAL in _app.tsx before any routing:\n\n// pages/_app.tsx\nimport { PublicClientApplication } from \"@azure/msal-browser\";\nimport { MsalProvider } from \"@azure/msal-react\";\nimport { msalConfig } from \"../auth/msal-config\";\n\n// Initialize outside component\nconst msalInstance = new PublicClientApplication(msalConfig);\n\n// 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 <div>Loading...</div>;\n\n  return (\n    <MsalProvider instance={msalInstance}>\n      <Component {...pageProps} />\n    </MsalProvider>\n  );\n}\n\n5. Safari/Edge Cookie Issues\n\nError: 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\nSource: GitHub Issue #7384\n\nCause: 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\nTesting Note: Works in Chrome on iOS 18, but fails in Safari on iOS 18.\n\nFix: Enable cookie storage in MSAL config:\n\ncache: {\n  cacheLocation: \"localStorage\",\n  storeAuthStateInCookie: true, // REQUIRED for Safari/Edge\n}\n\n\niOS 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\n6. JWKS URL Not Found (Workers)\n\nError: Failed to fetch JWKS from .well-known/jwks.json.\n\nCause: Azure AD doesn't serve JWKS at the standard OpenID Connect path.\n\nFix: Fetch openid-configuration first, then use jwks_uri:\n\n// WRONG - Azure AD doesn't use this path\nconst jwks = createRemoteJWKSet(\n  new URL(`https://login.microsoftonline.com/${tenantId}/.well-known/jwks.json`)\n);\n\n// CORRECT - Fetch config first\nconst config = await fetch(\n  `https://login.microsoftonline.com/${tenantId}/v2.0/.well-known/openid-configuration`\n).then(r => r.json());\nconst jwks = createRemoteJWKSet(new URL(config.jwks_uri));\n\n7. React Router Loader State Conflict\n\nError: React warning about updating state during render when using acquireTokenSilent in React Router loaders.\n\nSource: GitHub Issue #7068\n\nCause: Using the same PublicClientApplication instance in both the router loader and MsalProvider causes state updates during rendering.\n\nFix: Call initialize() again in the loader:\n\nconst protectedLoader = async () => {\n  await msalInstance.initialize(); // Prevents state conflict\n  const response = await msalInstance.acquireTokenSilent(request);\n  return { data };\n};\n\n8. setActiveAccount Doesn't Trigger Re-render (Community-sourced)\n\nError: Components using useMsal() don't update after calling setActiveAccount().\n\nSource: GitHub Issue #6989\n\nVerified: Multiple users confirmed in GitHub issue\n\nCause: setActiveAccount() updates the MSAL instance but doesn't notify React of the change.\n\nFix: Force re-render with state:\n\nconst [accountKey, setAccountKey] = useState(0);\n\nconst switchAccount = (newAccount) => {\n  msalInstance.setActiveAccount(newAccount);\n  setAccountKey(prev => prev + 1); // Force update\n};\n\nMulti-Tenant vs Single-Tenant\nSingle Tenant (Recommended for Enterprise)\nauthority: `https://login.microsoftonline.com/${TENANT_ID}`,\n\nOnly users from your organization can sign in\nToken issuer: https://login.microsoftonline.com/{tenant_id}/v2.0\nMulti-Tenant\nauthority: \"https://login.microsoftonline.com/common\",\n// or for work/school accounts only:\nauthority: \"https://login.microsoftonline.com/organizations\",\n\nUsers from any Azure AD tenant can sign in\nToken issuer varies by user's tenant\nBackend validation must handle multiple issuers:\n// Multi-tenant issuer validation\nconst tenantId = payload.tid; // Tenant ID from token\nconst expectedIssuer = `https://login.microsoftonline.com/${tenantId}/v2.0`;\nif (payload.iss !== expectedIssuer) {\n  throw new Error(\"Invalid issuer\");\n}\n\nEnvironment Variables\nFrontend (.env)\nVITE_AZURE_CLIENT_ID=your-client-id-guid\nVITE_AZURE_TENANT_ID=your-tenant-id-guid\n\nBackend (wrangler.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\nAzure AD B2C Sunset\n\nTimeline:\n\nMay 1, 2025: Azure AD B2C no longer available for new customer signups\nMarch 15, 2026: Azure AD B2C P2 discontinued for all customers\nMay 2030: Microsoft will continue supporting existing B2C customers with standard support\n\nSource: Microsoft Q&A\n\nExisting B2C Customers: Can continue using B2C until 2030, but should plan migration to Entra External ID.\n\nNew Projects: Use Microsoft Entra External ID for consumer/customer identity scenarios.\n\nMigration Status: As of January 2026, automated migration tools are in testing phase. Manual migration guidance available at Microsoft Learn.\n\nMigration Path:\n\nDifferent authority URL format ({tenant}.ciamlogin.com vs {tenant}.b2clogin.com)\nUpdated SDK support (same MSAL libraries)\nNew pricing model (consumption-based)\nSelf-Service Password Reset (SSPR) approach available for user migration\nSeamless migration samples on GitHub (preview)\n\nSee: 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\n\nADAL Retirement (Complete)\n\nStatus: Azure AD Authentication Library (ADAL) was retired on September 30, 2025. Apps using ADAL no longer receive security updates.\n\nIf you're migrating from ADAL:\n\nADAL → MSAL migration is required\nADAL used v1.0 endpoints; MSAL uses v2.0 endpoints\nToken cache format differs - users must re-authenticate\nScopes replace \"resources\" in token requests\n\nKey Migration Changes:\n\n// ADAL (deprecated) - resource-based\nacquireToken({ resource: \"https://graph.microsoft.com\" })\n\n// MSAL (current) - scope-based\nacquireTokenSilent({ scopes: [\"https://graph.microsoft.com/User.Read\"] })\n\n\nSee: https://learn.microsoft.com/en-us/entra/msal/javascript/migration/msal-net-migration\n\nResources\nMSAL React Documentation\nMicrosoft Entra ID App Registration\nMSAL.js GitHub Issues\njose Library\nCloudflare Workers + Azure AD Blog"
  },
  "trust": {
    "sourceLabel": "tencent",
    "provenanceUrl": "https://clawhub.ai/Veeramanikandanr48/azure-auth",
    "publisherUrl": "https://clawhub.ai/Veeramanikandanr48/azure-auth",
    "owner": "Veeramanikandanr48",
    "version": "0.1.0",
    "license": null,
    "verificationStatus": "Indexed source record"
  },
  "links": {
    "detailUrl": "https://openagent3.xyz/skills/azure-auth",
    "downloadUrl": "https://openagent3.xyz/downloads/azure-auth",
    "agentUrl": "https://openagent3.xyz/skills/azure-auth/agent",
    "manifestUrl": "https://openagent3.xyz/skills/azure-auth/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/azure-auth/agent.md"
  }
}