CORS errors are the most frustrating errors in OAuth development. The browser blocks your request, the error message is generic, and the actual cause could be any of 8+ different scenarios. This guide covers every CORS error you’ll encounter in OAuth 2.0 and OIDC flows, with exact browser error messages and provider-specific fixes.

Quick Diagnostic: Which Error Are You Seeing?

Browser Console ErrorJump To
No 'Access-Control-Allow-Origin' header on /authorizeScenario 1: Calling /authorize via fetch
No 'Access-Control-Allow-Origin' header on /tokenScenario 2: Token endpoint CORS
AADSTS9002327: Cross-origin token redemptionScenario 3: Azure AD SPA registration
CORS error only after session timeoutScenario 4: Keycloak error response bug
wildcard '*' when credentials mode is 'include'Scenario 5: Wildcard with credentials
Response to preflight request doesn't passScenario 6: Preflight failures
CORS error on /revoke endpointScenario 7: Token revocation
Everything works except in productionScenario 8: Proxy/CDN stripping headers

Which OAuth Endpoints Support CORS?

Before debugging, know which endpoints are designed to accept cross-origin requests:

EndpointCORS SupportAccess Method
/authorizeNoBrowser redirect (window.location.href)
/token (Auth Code + PKCE, public client)Yesfetch from SPA
/token (client_credentials)NoServer-to-server only
/userinfoYesfetch with Authorization: Bearer
/.well-known/openid-configurationYesfetch
/jwksYesfetch (used by OIDC libraries)
/revokeProvider-dependentPrefer server-side
/logoutNoBrowser redirect

Scenario 1: Calling /authorize via fetch

This is the #1 most common CORS error in OAuth. The /authorize endpoint is a browser-navigation endpoint — it responds with a 302 redirect to the login UI.

AfNcrocoem'sAsocrctieogsisXn-MCL'oHhntttttrppoRsle:-q/Au/lemlsyotawp-apOt.rei'xghaitmntp'plseh:.eca/odameu'rthhi.asesxpabrmeepeslneen.btclooomcn/koetadhuetbhyr2e/CqaOuuRetSshtoperodilzirece?ysc:oluirecnet._id=...'

Fix: Never call /authorize via fetch() or XMLHttpRequest. Use a browser redirect:

// WRONG — will always cause a CORS error
const response = await fetch(`${authServerUrl}/authorize?...`);

// CORRECT — browser redirect, bypasses CORS entirely
window.location.href = `${authServerUrl}/authorize?` + new URLSearchParams({
  response_type: 'code',
  client_id: CLIENT_ID,
  redirect_uri: REDIRECT_URI,
  scope: 'openid profile',
  code_challenge: codeChallenge,
  code_challenge_method: 'S256',
  state: state
});

This applies to every OAuth provider — Keycloak, Auth0, Okta, Azure AD, Google, ForgeRock, Cognito. No provider enables CORS on the authorize endpoint.

Scenario 2: Token Endpoint CORS Errors

The token endpoint /token supports CORS for public client flows (Authorization Code + PKCE) but NOT for confidential client flows (client_credentials).

Provider CORS Configuration

Keycloak: Admin Console → Clients → [Client] → Access Settings → Web Origins

https://myapp.example.comesxhaocrttcourti:gianll(onwoatlrlaivlailnigdsrleadsihr,ecntoUpRaItsh)asorigins

Common mistakes:

  • Leaving Web Origins blank (CORS fails for all requests)
  • Adding a trailing slash: https://myapp.example.com/ (invalid)
  • Adding a path: https://myapp.example.com/* (paths not allowed in origins)

Okta: Admin Console → Security → API → Trusted Origins

  1. Click “Add Origin”
  2. Enter https://myapp.example.com
  3. Check the CORS checkbox
  4. Save

Auth0: Dashboard → Applications → [App] → Settings → Allowed Origins (CORS)

https://myapp.example.com

Also set Allowed Web Origins if using Auth0’s cross-origin authentication.

ForgeRock AM: Configure → Global Services → CORS Service → Accepted Origins

Or configure per-client via the OAuth 2.0 client’s JavaScript Origins field — ForgeRock AM automatically adds these to the CORS allowlist.

AWS Cognito: No admin CORS configuration. Cognito’s token endpoint accepts cross-origin requests from any origin for public clients.

Scenario 3: Azure AD SPA Registration

AfAoDrSTtSh9e00'2S3i2n7g:leC-rPoasgse-oArpipgliincattoikoenn'rceldieemnpttitoynpei.spermittedonly

Azure AD / Microsoft Entra ID determines CORS eligibility based on the redirect URI platform type in the app registration — not by any CORS configuration setting.

Fix: Go to App Registration → Authentication → Add platform → Single-page application → add your redirect URI. If it’s currently registered under “Web”, move it to “Single-page application”.

Azure AD also blocks client_credentials from browsers entirely. If a browser sends an Origin header with a confidential flow, Entra rejects it to prevent secrets from leaking in client-side code.

Scenario 4: Keycloak CORS on Error Responses

This bug (KEYCLOAK-1886) causes CORS errors that appear only after session expiry (see Keycloak Session Timeout Configuration for session timeout details):

  1. User logs in → SPA exchanges code for tokens → works fine (200 with CORS headers)
  2. Session expires → SPA tries to refresh → Keycloak returns 400 invalid_grant without CORS headers
  3. Browser sees 400 + no Access-Control-Allow-Origin → throws CORS error
  4. The SPA never sees the actual invalid_grant — only a CORS error

Workaround with nginx:

location /realms/ {
    proxy_pass http://keycloak:8080;

    # 'always' ensures CORS headers on ALL responses including 4xx/5xx
    add_header 'Access-Control-Allow-Origin' 'https://myapp.example.com' always;
    add_header 'Access-Control-Allow-Credentials' 'true' always;
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
    add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' always;

    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' 'https://myapp.example.com';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type';
        add_header 'Access-Control-Max-Age' 86400;
        return 204;
    }
}

The key is the always directive — without it, nginx only adds headers to 2xx responses.

Scenario 5: Wildcard Origin with Credentials

Tthheevwailludecaorfdthe'wAhcecnestsh-eCornetqruoels-tA'lslocwr-eOdreingtiina'lshemaoddeeriisn'tihneclruedsep'o.nsemustnotbe

This happens when:

  • The server returns Access-Control-Allow-Origin: *
  • The client sends credentials: 'include' or withCredentials: true
  • The CORS spec forbids this combination

Fix: Replace * with the exact origin and add the credentials header:

AAcccceessss--CCoonnttrrooll--AAllllooww--OCrriegdienn:tihatltsp:s:t/r/umeyapp.example.com

In Keycloak, this means using the exact origin in Web Origins instead of *.

Scenario 6: Preflight Request Failures

A preflight OPTIONS request is triggered when your request uses:

  • Content-Type: application/json (not application/x-www-form-urlencoded)
  • Custom headers like Authorization or X-Custom-Header
  • HTTP methods other than GET, HEAD, or POST

Key insight: The token endpoint uses Content-Type: application/x-www-form-urlencoded by default. If you accidentally set Content-Type: application/json, you trigger a preflight that many OAuth servers don’t handle:

// WRONG — triggers unnecessary preflight
const response = await fetch(tokenEndpoint, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ grant_type: 'authorization_code', code, ... })
});

// CORRECT — no preflight needed (simple request)
const response = await fetch(tokenEndpoint, {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({ grant_type: 'authorization_code', code, ... })
});

Spring Security preflight fix:

http.cors().and()
    .csrf().disable()
    .authorizeRequests()
    .antMatchers(HttpMethod.OPTIONS, "/**").permitAll();

Scenario 7: Token Revocation CORS

Google’s /revoke endpoint does NOT support CORS. Calling it from a SPA via fetch always fails.

Workaround — form POST (bypasses CORS):

const form = document.createElement('form');
form.method = 'POST';
form.action = 'https://oauth2.googleapis.com/revoke';
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'token';
input.value = accessToken;
form.appendChild(input);
document.body.appendChild(form);
form.submit();

Auth0’s /oauth/revoke requires the app domain in Allowed Origins (CORS) in the dashboard.

Scenario 8: Proxy or CDN Stripping CORS Headers

Everything works locally but fails in production. Common causes:

  • CloudFlare / CDN caching a response without CORS headers, then serving it to cross-origin requests
  • nginx/Apache reverse proxy not forwarding the Origin header to the backend
  • Load balancer stripping Access-Control-* headers

Debug checklist:

# Test directly against the OAuth server (bypass proxy)
curl -v -H "Origin: https://myapp.example.com" \
  https://auth.example.com/realms/myrealm/protocol/openid-connect/token

# Check if CORS headers are in the response
# Look for: Access-Control-Allow-Origin: https://myapp.example.com

# Test through your proxy
curl -v -H "Origin: https://myapp.example.com" \
  https://your-proxy.example.com/auth/token

# Compare the headers — if the proxy response is missing CORS headers,
# the proxy is stripping them

When CORS Errors Mean Wrong Architecture

If you’re hitting CORS errors with client_credentials, on_behalf_of, or any confidential client flow from a SPA — the problem isn’t CORS configuration, it’s your architecture.

SPAs should NOT:

  • Hold client_secret in JavaScript
  • Call /token with client_credentials grant
  • Store refresh tokens in localStorage

Use the Backend for Frontend (BFF) pattern instead:

SSPPAA(s(asmaem-eo-roirgiignincocookoikei)e)BFFBFF(ser(vteork-etnos-ssetrovreerd)servOeAru-tshidPer)ovider

The BFF handles all OAuth token operations server-side. The SPA only communicates with the BFF using HttpOnly, SameSite cookies. No CORS issues because the SPA and BFF share the same origin.

For implementation details, see Integrating OAuth 2.0 with React SPA Using BFF.

Debugging with Browser DevTools

  1. Open DevTools → Network tab
  2. Filter by the failing request URL
  3. Check the Headers tab:
    • Request Headers: Look for Origin: https://myapp.example.com
    • Response Headers: Look for Access-Control-Allow-Origin
  4. If the response has no Access-Control-Allow-Origin, the server isn’t configured for your origin
  5. Check the Console tab for the exact CORS error message
  6. Look for a preceding OPTIONS request — if it failed or returned non-2xx, the preflight is the problem
# Replicate the request with curl to confirm it's a CORS issue (not a server error)
curl -v -X POST https://auth.example.com/token \
  -H "Origin: https://myapp.example.com" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code&code=CODE&redirect_uri=URI&client_id=ID&code_verifier=VERIFIER"

If curl succeeds (returns tokens), the server works but is missing CORS headers for your origin. Configure the provider as described above.