# Migrating from Keycloak-js

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](https://docs.oidc-spa.dev/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](https://docs.oidc-spa.dev/security-features/browser-runtime-freeze)
* (Optional) [Token Substitution](https://docs.oidc-spa.dev/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. &#x20;

**UX**

* [Auto Logout](https://docs.oidc-spa.dev/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](https://docs.oidc-spa.dev/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](https://docs.oidc-spa.dev/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](https://docs.oidc-spa.dev/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 %}
