# 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.

Source: https://oltinid.com/docs/guides/browser-applications/ · Section: Guides · All OneiD documentation: https://oltinid.com/llms.txt

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

```text
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](https://oltinid.com/docs/get-started/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.

```ts
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](https://oltinid.com/docs/quickstarts/javascript/), [React](https://oltinid.com/docs/quickstarts/react/) or [Angular](https://oltinid.com/docs/quickstarts/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](https://oltinid.com/docs/quickstarts/) 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](https://oltinid.com/docs/guides/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](https://oltinid.com/docs/guides/sessions-and-reauthentication/).

## Troubleshooting CORS

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

```text
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

- [Authorization code flow with PKCE](https://oltinid.com/docs/guides/authorization-code-pkce/)
- [Refresh tokens](https://oltinid.com/docs/guides/refresh-tokens/)
- [Security best practices](https://oltinid.com/docs/guides/security-best-practices/)
- [Quickstarts](https://oltinid.com/docs/quickstarts/)
