Browser applications and CORS

Connect a single-page application to OneiD as a public client, store tokens safely, renew them in the background and fix CORS errors.

View as Markdown

A single-page application (SPA) runs entirely in the user’s browser and talks to OneiD directly. It is a public client: it has no secret, uses the authorization code flow with PKCE, and calls the token endpoint from the browser, which needs CORS. This guide covers the setup, token storage, background renewal and the CORS errors you may meet.

Architecture

Browser (your SPA)                 OneiD                         Your API
------------------                 -----                         --------
1. Redirect to /connect/authorize ->
                                   2. User signs in
3. <- Redirect to callback with code
4. POST /connect/token (CORS) ---->
5. <- ID token, access token, refresh token
6. GET /orders, Authorization: Bearer ... --------------------->  7. Validate token

Steps 1 and 3 are browser navigations, so CORS does not apply. Step 4 is a cross-origin fetch, so OneiD must allow your application’s origin.

What to ask your administrator for

  • Client type public. PKCE is always required.
  • Grant types: authorization code, and refresh token if you want background renewal.
  • Redirect URIs, for example https://app.example.com/callback and http://localhost:4200/callback.
  • Post-logout redirect URIs, for example https://app.example.com/signed-out.
  • Allowed scopes, for example openid profile email offline_access orders.read.
  • Allowed CORS origins: every origin your SPA is served from, written scheme://host[:port] with no path, for example https://app.example.com and http://localhost:4200.

See Register an application for the full template.

Which endpoints allow cross-origin requests

Endpoint Cross-origin requests
Discovery, /.well-known/openid-configuration Any origin
JWKS, /.well-known/jwks Any origin
Token, /connect/token Registered origins on an enabled client only
Userinfo, /connect/userinfo Registered origins on an enabled client only
Revocation, /connect/revoke Registered origins on an enabled client only
Introspection, /connect/introspect Never
Authorize and end session Not applicable: these are navigations, not fetch calls

CORS requests never carry cookies. The SPA authenticates to these endpoints with tokens and its client ID only.

Configure a library

Use a maintained OpenID Connect library rather than writing the flow yourself. Common choices:

  • Plain JavaScript or TypeScript: oidc-client-ts
  • React: react-oidc-context, which wraps oidc-client-ts
  • Angular: oidc-client-ts in an Angular service

These libraries work with any standards-based provider. The examples below are a starting point, not an endorsement of a particular version.

import { UserManager, WebStorageStateStore, InMemoryWebStorage } from 'oidc-client-ts';

export const userManager = new UserManager({
  authority: 'https://YOUR_ONEID',
  client_id: 'YOUR_CLIENT_ID',
  redirect_uri: 'https://app.example.com/callback',
  post_logout_redirect_uri: 'https://app.example.com/signed-out',
  response_type: 'code',
  scope: 'openid profile email offline_access orders.read',
  // Keep tokens in memory, not in localStorage.
  userStore: new WebStorageStateStore({ store: new InMemoryWebStorage() }),
  // Renew before expiry with the refresh token (request offline_access).
  // Without a refresh token the library falls back to a hidden iframe,
  // which OneiD does not allow.
  automaticSilentRenew: true,
});

// Sign in:   await userManager.signinRedirect();
// Callback:  await userManager.signinRedirectCallback();
// Sign out:  await userManager.signoutRedirect();

The library reads the endpoints from discovery, creates the PKCE values, state and nonce, and validates the ID token. It compares the ID token’s iss with the issuer from discovery, https://YOUR_ONEID/, including the trailing slash.

Checkpoint After sign-in, the browser’s network panel shows a POST to https://YOUR_ONEID/connect/token with status 200, and your application shows the signed-in user’s name.

For a step-by-step setup, follow the JavaScript, React or Angular quickstart.

Store tokens safely

Anything a script on your page can read, an injected script can read too.

  • Keep tokens in memory. A JavaScript variable or the library’s in-memory store is the safest place in the browser.
  • Avoid localStorage for refresh tokens. A refresh token there survives the session and can be read by any script that runs on your origin. sessionStorage has the same exposure while the tab is open.
  • Accept the trade-off. With in-memory storage, reloading the page loses the tokens. The library then sends the user to OneiD again; while the OneiD session lasts, that completes without a password prompt.
  • Consider a backend-for-frontend. For applications that handle sensitive data, run a small server-side component as a confidential client. It holds the tokens, calls the API, and gives the browser only an HttpOnly, Secure, SameSite session cookie. The browser then never sees a token. Use a server-side quickstart for that component.
  • Set a Content Security Policy on your SPA that allows connect-src to https://YOUR_ONEID and your API, and restricts scripts to your own origin.

Renew tokens in the background

Access tokens last 1 hour by default. To keep the user signed in without a redirect, request offline_access and let the library renew with the refresh token before the access token expires.

OneiD rotates refresh tokens on every use, and using one twice after a grace period of about 30 seconds revokes the whole chain. In a browser this matters when several tabs share one refresh token:

  • With in-memory storage, each tab signs in on its own and holds its own chain, so tabs do not collide.
  • If you share tokens between tabs, let only one tab refresh at a time and pass the new tokens to the others.

When a refresh returns invalid_grant, the session is over: clear the tokens and start a new sign-in. See Refresh tokens.

Do not use hidden iframes with prompt=none for renewal. OneiD pages cannot be framed, so iframe-based silent renewal does not work. Use a refresh token, or send the whole page to OneiD with prompt=none as a top-level redirect. See Sessions, prompt and max_age.

Troubleshooting CORS

A CORS failure appears in the browser console, not as an OAuth error. It looks similar to this:

Access to fetch at 'https://YOUR_ONEID/connect/token' from origin 'https://app.example.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.

Check, in order:

  1. The origin is registered exactly. Compare the origin in the error with the client’s allowed CORS origins: scheme, host and port must match. http://localhost:4200 and http://127.0.0.1:4200 are different origins. Do not include a path or a trailing slash.
  2. The client is enabled. OneiD answers cross-origin requests only for origins on an enabled client.
  3. Wait briefly after a change. A newly registered origin takes effect within about 15 seconds.
  4. You are not calling introspection. Introspection never allows cross-origin requests. A browser application does not need it; your API validates tokens.
  5. The request reached OneiD at all. If the network panel shows no response, check that the address is https://YOUR_ONEID and that a proxy or content blocker is not interfering.

If the token request succeeds but your API call fails with a CORS error, the problem is in your API’s CORS settings, not OneiD’s.

Learn more