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.
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/callbackandhttp://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 examplehttps://app.example.comandhttp://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 wrapsoidc-client-ts - Angular:
oidc-client-tsin 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
POSTtohttps://YOUR_ONEID/connect/tokenwith 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
localStoragefor refresh tokens. A refresh token there survives the session and can be read by any script that runs on your origin.sessionStoragehas 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,SameSitesession 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-srctohttps://YOUR_ONEIDand 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:
- 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:4200andhttp://127.0.0.1:4200are different origins. Do not include a path or a trailing slash. - The client is enabled. OneiD answers cross-origin requests only for origins on an enabled client.
- Wait briefly after a change. A newly registered origin takes effect within about 15 seconds.
- You are not calling introspection. Introspection never allows cross-origin requests. A browser application does not need it; your API validates tokens.
- The request reached OneiD at all. If the network panel shows no response, check that the address is
https://YOUR_ONEIDand 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.