{
  "schemaVersion": "1.0",
  "code": "9002327",
  "name": null,
  "title": "AADSTS9002327: SPA token redeemed without a browser Origin (fix)",
  "url": "https://brambrinkmeier.com/entra/aadsts/9002327/",
  "alternates": {
    "markdown": "https://brambrinkmeier.com/entra/aadsts/9002327.md",
    "json": "https://brambrinkmeier.com/api/aadsts/9002327.json",
    "html": "https://brambrinkmeier.com/entra/aadsts/9002327/"
  },
  "description": "AADSTS9002327: an SPA's code or refresh token was redeemed without a browser Origin header. Not in Microsoft's error list. Fixes and KQL.",
  "summary": "AADSTS9002327 means a token request used an authorization code or refresh token issued to a single-page app, but the request had no Origin header, so it didn't come from a browser. Microsoft's error reference doesn't list it. A Q&A answerer links it to redemption from server-side code. Low attack value. Redeem in the browser, or register a Web platform.",
  "inMicrosoftReference": false,
  "unlistedChecked": "2026-10-30",
  "meaningText": "Microsoft's [error code reference](https://learn.microsoft.com/entra/identity-platform/reference-error-codes) doesn't list AADSTS9002327 (checked 30 October 2026).\n\nThe wording below comes from user reports. A user on Microsoft Q&A posted this token endpoint response (`400 Bad Request`, `invalid_request`):\n\n> AADSTS9002327: Tokens issued for the 'Single-Page Application' client-type may only be redeemed via cross-origin requests.\n\nThe app was registered with the **Single-page application** platform and used the authorization code flow with PKCE. A Microsoft moderator answered that the error is thrown *\"due to your token requests missing the Origin header\"*, and gave two fixes: add the header, or change the redirect URI platform to **Web** or **Native/Desktop**. Another answerer, not marked as Microsoft staff, wrote: *\"Usually, you get the AADSTS9002327 error when trying to exchange a refresh token from server-side code.\"*. The thread doesn't confirm which applied.\n\nMicrosoft documents the rule, without the code number, in its authorization code flow reference:\n\n> Applications can't use a `spa` redirect URI with non-SPA flows, for example, native applications or client credential flows. To ensure security and best practices, the Microsoft identity platform returns an error if you attempt to use a `spa` redirect URI without an `Origin` header.\n\nOur reading: 9002327 is that error. A code or refresh token issued through a `spa` redirect URI can only be redeemed by a request that carries an `Origin` header, which is what a browser sends. It is the mirror of [9002326](https://brambrinkmeier.com/entra/aadsts/9002326/), where a request with an `Origin` header hits an app that isn't an SPA.",
  "sections": [
    {
      "heading": "What does AADSTS9002327 mean?",
      "text": "Microsoft's [error code reference](https://learn.microsoft.com/entra/identity-platform/reference-error-codes) doesn't list AADSTS9002327 (checked 30 October 2026).\n\nThe wording below comes from user reports. A user on Microsoft Q&A posted this token endpoint response (`400 Bad Request`, `invalid_request`):\n\n> AADSTS9002327: Tokens issued for the 'Single-Page Application' client-type may only be redeemed via cross-origin requests.\n\nThe app was registered with the **Single-page application** platform and used the authorization code flow with PKCE. A Microsoft moderator answered that the error is thrown *\"due to your token requests missing the Origin header\"*, and gave two fixes: add the header, or change the redirect URI platform to **Web** or **Native/Desktop**. Another answerer, not marked as Microsoft staff, wrote: *\"Usually, you get the AADSTS9002327 error when trying to exchange a refresh token from server-side code.\"*. The thread doesn't confirm which applied.\n\nMicrosoft documents the rule, without the code number, in its authorization code flow reference:\n\n> Applications can't use a `spa` redirect URI with non-SPA flows, for example, native applications or client credential flows. To ensure security and best practices, the Microsoft identity platform returns an error if you attempt to use a `spa` redirect URI without an `Origin` header.\n\nOur reading: 9002327 is that error. A code or refresh token issued through a `spa` redirect URI can only be redeemed by a request that carries an `Origin` header, which is what a browser sends. It is the mirror of [9002326](https://brambrinkmeier.com/entra/aadsts/9002326/), where a request with an `Origin` header hits an app that isn't an SPA."
    },
    {
      "heading": "Is it an attack?",
      "text": "No. It is a mismatch between how the app is registered and where it redeems tokens.\n\n| Pattern | Likely meaning |\n|---|---|\n| A developer testing the token call in Postman or curl | No `Origin` header is sent. The SPA registration rejects it. Likely, but unconfirmed. |\n| An app that renders on the server (SSR, Blazor Server, MVC) registered as SPA | One Q&A answerer suggests this: the code is redeemed by server code, not the browser. |\n| A backend that takes the SPA's refresh token and refreshes it itself | One Q&A answerer calls this the usual cause. The refresh token stays bound to the browser. |\n| A sudden spike after a deployment | Probably token redemption moved from the browser to a backend or proxy (unconfirmed). |\n\nOur reading: the check is a defence. A code or refresh token issued to a browser app can't simply be redeemed from a script without the `Origin` header. Anyone can set that header in a script, though, so it is not a strong barrier. Don't treat it as one."
    },
    {
      "heading": "How do I find it in my logs (KQL)?",
      "text": "Microsoft doesn't document which sign-in table 9002327 is logged in, or whether it is logged at all. Check where it lands in your workspace:\n\n```kusto\nunion withsource=SourceTable SigninLogs, AADNonInteractiveUserSignInLogs, AADServicePrincipalSignInLogs\n| where TimeGenerated > ago(30d)\n| where ResultType == \"9002327\"\n| summarize Rows = count() by SourceTable\n```\n\nIf it does land in a user sign-in table, find the apps and the clients behind it. A server or tool user agent instead of a browser points at server-side redemption:\n\n```kusto\nunion SigninLogs, AADNonInteractiveUserSignInLogs\n| where TimeGenerated > ago(30d)\n| where ResultType == \"9002327\"\n| summarize Rows = count(),\n            Users = dcount(UserPrincipalName),\n            UserAgents = make_set(UserAgent, 10),\n            IPs = make_set(IPAddress, 10)\n          by AppDisplayName, AppId\n| sort by Rows desc\n```"
    },
    {
      "heading": "How do I fix AADSTS9002327?",
      "text": "**For the user:** you can't fix this yourself. Report the app name to whoever runs it.\n\n**For developers:**\n1. Decide where tokens should be redeemed. If the browser does it, use MSAL.js in the browser and let it make the token call. The browser sends the `Origin` header.\n2. If your server redeems the code or refreshes tokens, the app isn't an SPA. Register a **Web** redirect URI and authenticate the server as a confidential client. That is one of the two fixes the Microsoft moderator gave.\n3. The moderator's other fix was to add the `Origin` header. Our reading: fine for a test in Postman or curl. Don't ship a backend that fakes a browser header to get around the SPA rules.\n4. Don't mix flows on one redirect URI. Microsoft: *\"Applications can't use a `spa` redirect URI with non-SPA flows.\"*\n\n**For admins:**\n1. Look at the app registration's **Authentication** page. Check which platform the failing redirect URI is under: **Single-page application**, **Web**, or **Mobile and desktop applications**.\n2. If the app's backend does the redemption, move that redirect URI to **Web**, with the developer's agreement. Moving it changes how the app must authenticate."
    }
  ],
  "microsoftWording": {
    "text": "AADSTS9002327: Tokens issued for the 'Single-Page Application' client-type may only be redeemed via cross-origin requests.",
    "source": "Quoted on the page from the lab or from user reports; the code is not in Microsoft's error code reference",
    "sourceUrl": null
  },
  "securityReading": {
    "category": {
      "id": "app-config",
      "label": "App configuration"
    },
    "kind": {
      "id": "config",
      "label": "Configuration error"
    },
    "attackSignal": {
      "level": "low",
      "label": "Low: usually configuration or user error"
    },
    "mitre": [],
    "tables": [],
    "isItAnAttack": "No. It is a mismatch between how the app is registered and where it redeems tokens.\n\n| Pattern | Likely meaning |\n|---|---|\n| A developer testing the token call in Postman or curl | No `Origin` header is sent. The SPA registration rejects it. Likely, but unconfirmed. |\n| An app that renders on the server (SSR, Blazor Server, MVC) registered as SPA | One Q&A answerer suggests this: the code is redeemed by server code, not the browser. |\n| A backend that takes the SPA's refresh token and refreshes it itself | One Q&A answerer calls this the usual cause. The refresh token stays bound to the browser. |\n| A sudden spike after a deployment | Probably token redemption moved from the browser to a backend or proxy (unconfirmed). |\n\nOur reading: the check is a defence. A code or refresh token issued to a browser app can't simply be redeemed from a script without the `Origin` header. Anyone can set that header in a script, though, so it is not a strong barrier. Don't treat it as one.",
    "conditionalAccess": null
  },
  "kql": [
    {
      "title": null,
      "leadIn": "Microsoft doesn't document which sign-in table 9002327 is logged in, or whether it is logged at all. Check where it lands in your workspace:",
      "query": "union withsource=SourceTable SigninLogs, AADNonInteractiveUserSignInLogs, AADServicePrincipalSignInLogs\n| where TimeGenerated > ago(30d)\n| where ResultType == \"9002327\"\n| summarize Rows = count() by SourceTable"
    },
    {
      "title": null,
      "leadIn": "If it does land in a user sign-in table, find the apps and the clients behind it. A server or tool user agent instead of a browser points at server-side redemption:",
      "query": "union SigninLogs, AADNonInteractiveUserSignInLogs\n| where TimeGenerated > ago(30d)\n| where ResultType == \"9002327\"\n| summarize Rows = count(),\n            Users = dcount(UserPrincipalName),\n            UserAgents = make_set(UserAgent, 10),\n            IPs = make_set(IPAddress, 10)\n          by AppDisplayName, AppId\n| sort by Rows desc"
    }
  ],
  "kqlSectionText": "Microsoft doesn't document which sign-in table 9002327 is logged in, or whether it is logged at all. Check where it lands in your workspace:\n\n```kusto\nunion withsource=SourceTable SigninLogs, AADNonInteractiveUserSignInLogs, AADServicePrincipalSignInLogs\n| where TimeGenerated > ago(30d)\n| where ResultType == \"9002327\"\n| summarize Rows = count() by SourceTable\n```\n\nIf it does land in a user sign-in table, find the apps and the clients behind it. A server or tool user agent instead of a browser points at server-side redemption:\n\n```kusto\nunion SigninLogs, AADNonInteractiveUserSignInLogs\n| where TimeGenerated > ago(30d)\n| where ResultType == \"9002327\"\n| summarize Rows = count(),\n            Users = dcount(UserPrincipalName),\n            UserAgents = make_set(UserAgent, 10),\n            IPs = make_set(IPAddress, 10)\n          by AppDisplayName, AppId\n| sort by Rows desc\n```",
  "fix": {
    "short": "Developer: if the browser redeems tokens, let MSAL.js in the browser make the token call. If your server redeems the code or refreshes tokens, register a Web redirect URI and authenticate the server as a confidential client. Don't fake the `Origin` header in a backend. User: report the app name.",
    "steps": "**For the user:** you can't fix this yourself. Report the app name to whoever runs it.\n\n**For developers:**\n1. Decide where tokens should be redeemed. If the browser does it, use MSAL.js in the browser and let it make the token call. The browser sends the `Origin` header.\n2. If your server redeems the code or refreshes tokens, the app isn't an SPA. Register a **Web** redirect URI and authenticate the server as a confidential client. That is one of the two fixes the Microsoft moderator gave.\n3. The moderator's other fix was to add the `Origin` header. Our reading: fine for a test in Postman or curl. Don't ship a backend that fakes a browser header to get around the SPA rules.\n4. Don't mix flows on one redirect URI. Microsoft: *\"Applications can't use a `spa` redirect URI with non-SPA flows.\"*\n\n**For admins:**\n1. Look at the app registration's **Authentication** page. Check which platform the failing redirect URI is under: **Single-page application**, **Web**, or **Mobile and desktop applications**.\n2. If the app's backend does the redemption, move that redirect URI to **Web**, with the developer's agreement. Moving it changes how the app must authenticate."
  },
  "labReproduced": null,
  "labSectionText": null,
  "labRuns": [
    {
      "date": "2026-10-30",
      "run": "round 5",
      "method": "token-endpoint requests against a lab tenant, using lab-created test apps",
      "issue": "https://github.com/GrantZero604/Brambrinkmeier.com/issues/69",
      "scenario": "SPA-registered client redeems without an Origin header",
      "expected": "9002327",
      "got": "65001",
      "matchedExpectation": false,
      "message": "AADSTS65001: The user or administrator has not consented to use the application with ID '{app-id}' named 'lab-spa-41136574530'. Send"
    },
    {
      "date": "2026-10-30",
      "run": "round 6",
      "method": "token-endpoint requests against a lab tenant, using lab-created test apps",
      "issue": "https://github.com/GrantZero604/Brambrinkmeier.com/issues/70",
      "scenario": "SPA-registered client redeems without an Origin header",
      "expected": "9002327 / 65001",
      "got": "65001",
      "matchedExpectation": true,
      "message": "AADSTS65001: The user or administrator has not consented to use the application with ID '{app-id}' named 'lab-spa-41139925607'. Send"
    },
    {
      "date": "2026-10-30",
      "run": "round 7",
      "method": "token-endpoint requests against a lab tenant",
      "issue": "https://github.com/GrantZero604/Brambrinkmeier.com/issues/71",
      "scenario": "SPA-registered client redeems without an Origin header",
      "expected": "9002327 / 65001",
      "got": "65001",
      "matchedExpectation": true,
      "message": "AADSTS65001: The user or administrator has not consented to use the application with ID '{app-id}' named 'lab-spa-41143330001'. Send"
    },
    {
      "date": "2026-10-31",
      "run": "round 8",
      "method": "token-endpoint and device-code requests against a lab tenant",
      "issue": "https://github.com/GrantZero604/Brambrinkmeier.com/issues/72",
      "scenario": "SPA-registered client redeems without an Origin header",
      "expected": "9002327 / 65001",
      "got": "65001",
      "matchedExpectation": true,
      "message": "AADSTS65001: The user or administrator has not consented to use the application with ID '{app-id}' named 'lab-spa-41146039117'. Send"
    },
    {
      "date": "2026-10-31",
      "run": "round 9",
      "method": "token-endpoint and device-code requests against a lab tenant",
      "issue": "https://github.com/GrantZero604/Brambrinkmeier.com/issues/73",
      "scenario": "SPA-registered client redeems without an Origin header",
      "expected": "9002327 / 65001",
      "got": "65001",
      "matchedExpectation": true,
      "message": "AADSTS65001: The user or administrator has not consented to use the application with ID '{app-id}' named 'lab-spa-41150019626'. Send"
    }
  ],
  "verification": "documentation",
  "related": [
    "9002326",
    "700054",
    "9002313",
    "7000218",
    "700081"
  ],
  "coveredBy": [],
  "sources": [
    {
      "title": "Microsoft Entra authentication and authorization error codes",
      "url": "https://learn.microsoft.com/entra/identity-platform/reference-error-codes"
    },
    {
      "title": "Microsoft Q&A: AADSTS9002327: Tokens issued for the 'Single-Page Application' client-type may only be redeemed via cross-origin requests (1601797)",
      "url": "https://learn.microsoft.com/en-us/answers/questions/1601797/aadsts9002327-tokens-issued-for-the-single-page-ap"
    },
    {
      "title": "Microsoft identity platform and OAuth 2.0 authorization code flow",
      "url": "https://learn.microsoft.com/entra/identity-platform/v2-oauth2-auth-code-flow"
    },
    {
      "title": "SigninLogs table reference (Azure Monitor)",
      "url": "https://learn.microsoft.com/en-us/azure/azure-monitor/reference/tables/signinlogs"
    }
  ],
  "published": "2026-10-30",
  "lastUpdated": "2026-10-30",
  "lastVerified": null,
  "license": "CC BY 4.0",
  "licenseUrl": "https://creativecommons.org/licenses/by/4.0/",
  "licenseNote": "CC BY 4.0 covers the author's own text and classification fields. Quoted Microsoft error wording belongs to Microsoft, is quoted with its source and can change.",
  "attribution": "Bram Brinkmeier, \"AADSTS9002327: SPA token redeemed without a browser Origin (fix)\", https://brambrinkmeier.com/entra/aadsts/9002327/ (CC BY 4.0)",
  "microsoftErrorLookup": "https://login.microsoftonline.com/error?code=9002327"
}
