Only this pageAll pages
Powered by GitBook
1 of 29

v6

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Setup Guides

Loading...

Loading...

Loading...

Providers Configuration

Loading...

Loading...

Loading...

Loading...

Loading...

Resources

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Installation

Before starting be aware that oidc-spa is not suited for Next.js.

If you are using Next the closer alternative is to use NextAuth.js (with the Keycloak adapter if you are using Keycloak). See this guide.

If you're having issues don't hesitate to reach out on Discord!

Add the lib to your dependencies

Editing your App entrypoint

This is optional but recommended for .

First rename your entry point file from main.tsx (or main.ts) to main.lazy.tsx

Then create a new main.tsx file:

Comming soon, .

You can skip this for now. It will be explained in the dedicated setup guide:

First rename your entry point file from main.tsx

User Impersonation

An example on how to implement user impersonation with Keycloak:

Why oidcEarlyInit

The standard OIDC flow with PKCE is sometimes perceived as less secure than the traditional "backend for frontend" (BFF) flow, where token exchanges happen on the backend and frontend code never has direct access to tokens.

This perception stems mainly from the nature of JavaScript projects, especially SPAs, which often bundle tens of thousands of npm packages from many different authors. If even one of those libraries is compromised, it could potentially steal user tokens unless specific countermeasures are in place. There's also the risk of cross-site scripting (XSS) attacks which, if successful, could lead to the same outcome.

The early init setup recommended in oidc-spa is designed to mitigate these risks. By letting a minimal piece of oidc-spa run before any other JavaScript code, the library ensures that:

  • The response from the authorization server is removed from the URL and stored in memory (scoped variable), so even if malicious code runs later, it can’t access the tokens.

  • Critical browser APIs, like window.fetch, are frozen to prevent malicious code from intercepting or tampering with authenticated requests.

An added (though minor) benefit of the early init + lazy loading setup is improved performance during silent SSO operations in iframes. In the iframe, only a minimal part of the app is evaluated—just the part that needs to forward the auth response to the parent window. Since JS files in SPAs are typically hashed and cached, the benefit isn't about download time but about reducing JavaScript evaluation time. That said, the gain is usually marginal—just a few extra milliseconds in most cases.

Is early init mandatory?

No. If not explicitly called, it will be invoked automatically as soon as oidc-spa is evaluated, which, in most setups, happens early enough.

Can it be skipped?

Yes, if:

  • The application is free of XSS vulnerabilities.

  • The dependencies can be fully trusted.

  • Performance at initialization time is not critical.

In such cases, skipping the setup is reasonable.

Important caveat for React Router (framework mode)

If using React Router in framework mode, then the early init setup must be used. React Router performs early redirects that can interfere with oidc-spa's initialization under certain circumstances. To avoid issues, oidc-spa must run first.

If the project is using Vite or Create React App without a higher-level framework, this caveat does not apply.

(or
main.ts
) to
main.lazy.tsx

Then create a new index.tsx file:

better performances and security
follow progress
React Router
npm install oidc-spa
yarn add oidc-spa
pnpm add oidc-spa
bun add oidc-spa
mv src/main.tsx src/main.lazy.tsx
src/main.tsx
import { oidcEarlyInit } from "oidc-spa/entrypoint";

const { shouldLoadApp } = oidcEarlyInit({
    freezeFetch: true,
    freezeXMLHttpRequest: true
});

if (shouldLoadApp) {
    import("./main.lazy");
}
mv src/index.tsx src/index.lazy.tsx
src/index.tsx
import { oidcEarlyInit } from "oidc-spa/entrypoint";

const { shouldLoadApp } = oidcEarlyInit({
    freezeFetch: true,
    freezeXMLHttpRequest: true
});

Sponsors

Backers of the project

iframe related issues

By default, applications using oidc-spa will create an iframe pointing to themselves in order to quickly restore the user’s session across reloads and navigations.

However, iframes have a bad reputation. They are sometimes considered an attack vector, and certain system engineers may prefer to forbid their usage entirely rather than applying more nuanced policies.

If your application is served with response headers such as:

Content-Security-Policy: frame-ancestors "none"

or

X-Frame-Options: DENY

...then your operations team has completely blocked iframe usage, even your SPA is not allowed to iframe itself.

In this scenario, you have two options :

TanStack Router

Vite + TypeScript + React + Tanstack Router

TanStack Start support is comming soon. .

The example setup is live here:

Run it locally with:

if (shouldLoadApp) {
import("./index.lazy");
}
Option 1: Enable the noIframe mode of oidc-spa

oidc-spa provides an option to disable iframe usage:

Note: this will slightly increase the initialization time of your application. Everything will still work as expected, but you won't have the fastest possible startup.

Here you have a comparison of the session resoration process with iframe and without iframe under poor network condition (the auth server is slow to respond). As you can see, with no iframe there is two consecutive page reload when with ifram everthing is done in the background.

Option 2: Adjust your security policy to allow iframe usage in this context

If possible, request a change in the security policy from your ops team. Instead of strict policies like:

  • Content-Security-Policy: frame-ancestors 'none'

  • X-Frame-Options: DENY

...you can use a more permissive directive like:

  • Content-Security-Policy: frame-ancestors 'self'

If you'd like to allow iframe usage only in the specific case where the app is iframing itself with typical OIDC query parameters, you can use conditional logic in your reverse proxy:

This configuration allows iframes only when the request query string includes the expected OIDC parameters, typically when the app is restoring a session by iframing itself.

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
yarn
yarn dev
Follow progress
https://example-tanstack-router.oidc-spa.dev/

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.

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
src/oidc.ts
import { createReactOidc } from "oidc-spa/react";

export const {
    /* ... */
    getOidc
} = createReactOidc({ /* ... */ });


You can also do this in your React component (although it's maybe not the best approach)

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 by the fact that oidc-spa only provides the decoded id token but not the decoded access token.

Infact the access token is supposed to be opaque for the client application is to be used only as an authentication key such as a Bearer token for an API.

As per the OIDC standard the access token is not even required to be a JWT!

But worry not, everything that you need is probably in the id token, if there is something missing in your id token that is present in your access token there is an explicit policy on your identity server in place that strips this information out. Zod is stripping out all the claims that are not specified in the schema. This might have led you to believe that there is less information in the id token than what actually is.

If, however, you still want to access the informations in the access token you can do it with:

import { decodeJwt } from "oidc-spa/tools/decodeJwt";

const decodedAccessToken = decodeJwt(oidc.getTokens().accessToken);
import { decodeJwt } from "oidc-spa/tools/decodeJwt";

const { oidcTokens } = useOidc();

const decodedAccessToken = decodeJwt(oidcTokens.accessToken);

Auto Login

Enforce authentication everywhere in your app.

If your application requires users to be authenticated at all times—such as dashboards or admin panels—you can configure oidc-spa to automatically redirect unauthenticated users to the login page. This ensures that no part of your app is accessible without authentication.

This is similar to wrapping your root component with withLoginEnforced(), but with a key difference: oidc-spa assumes the user will never be unauthenticated. This means you do not need to:

  • Check isUserLoggedIn, as it will always be true.

Auto Logout

Automatically logging out your user after a set period of inactivity on your app (they dont move the mouse or press any key on the keyboard for a while)

Configuring auto logout policy

Important to understand: This is a policy that is enforced on the identity server. Not in the application code!

In OIDC provider, it is usually referred to as Idle Session Lifetime, these values define how long an inactive session should be kept in the records of the server.

Guide on how to configure it:

Mock

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.

End of third-party cookies

Third-party cookies are now blocked on most browsers, including Chrome, Safari, and Firefox.

Your OIDC provider is considered a third party relative to your application if they do not share a common parent domain.

How Does This Affect You?

Even if your OIDC provider is treated as a third party by the browser, in most cases, this does not impact functionality. oidc-spa works seamlessly even if auth cookies are blocked.

src/oidc.ts
createReactOidc({
  // ...
  noIframe: true
});
nginx.conf
-add_header X-Frame-Options "DENY";
-add_header Content-Security-Policy "frame-ancestors 'none'";
+add_header Content-Security-Policy "frame-ancestors 'self'";
nginx.conf
map $query_string $add_content_security_policy {
  "~*(?=.*\bstate=)(?=.*\bclient_id=)(?=.*\bresponse_type=)(?=.*\bredirect_uri=)" "frame-ancestors 'self'";
  default "frame-ancestors 'none'";
}
add_header Content-Security-Policy $add_content_security_policy;
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)
        }
    
    }, []);
// 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)
}
}
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)
}
});

(React) Use the assertion useOidc({ assert: "user logged in" }), since the user is guaranteed to be logged in.

  • (React) Use withLoginEnforced, it is not exposed in this mode since it is always enforced.

  • (React) You don't need to call enforceLogin() in your loaders.

  • import { createOidc, type OidcInitializationError } from "oidc-spa";
    
    const oidc = await createOidc({
        // ...
        autoLogin: true,
        // postLoginRedirectUrl: "/dashboard"
    
    
    src/oidc.ts
    import { createReactOidc } from "oidc-spa/react";
    
    export const { OidcProvider, useOidc, getOidc } = createReactOidc({
       // ...
       autoLogin
    

    Handling Initialization Errors

    In this mode, initialization errors must be handled at the <OidcProvider> level.

    src/main.tsx
    import React from "react";
    import ReactDOM from "react-dom/client";
    import { OidcProvider } from "oidc";
    
    const router = createRouter
    

    Keycloak

  • Auth0

  • If your OIDC provider issues a Refresh Token and if this refresh token is a JWT you don't need to configure anything at the app level. Otherwise you need to explicitly set the idleSessionLifetimeInSeconds so it matches with how you have configured your server.

    import { createOidc } from "oidc-spa";
    
    const oidc = await createOidc({
      // ...
      // ‼️ WARNING ‼️ Read carfully what's above.
      // Use idleSessionLifetimeInSeconds if and only if you are using an auth server
      // that do not let you configure this policy! (e.g. if you're using Keycloak don't use this param) 
    
    
    import { createReactOidc } from "oidc-spa/react";
    
    export const {
        OidcProvider,
        useOidc
    } = createReactOidc({
        // ...
        __unsafe_ssoSessionIdleSeconds: 300 
    

    Displaying a countdown timer before auto logout

    Example implementation of a 60 seconds countdown before auto logout.

    However, if your app is allowed to set cookies on your OIDC provider’s domain, you may see a slight performance improvement during the initial load. This is because we can use silent sign-in via an iframe instead of requiring a full page reload.

    The only scenario where blocked cookies significantly degrade the user experience is when both of the following conditions are true:

    • Your OIDC provider does not issue refresh tokens.

    • The access token has a short lifespan.

    For example, if the access token expires every 20 seconds, your app will be forced to reload every 18 seconds, which is not ideal.

    To avoid this issue, ensure that your OIDC provider shares a common parent domain with your app so that browsers do not treat it as a third party.

    When Are Cookies Considered Third-Party?

    Third-party cookie restrictions apply when your OIDC provider and your application do not share a parent domain.

    Examples:

    ✅ No third-party cookie issues (Same Parent Domain)

    • App hosted at www.my-company.com, dashboard.my-company.com, or my-company.com/dashboard

    • issuerUri: https://auth.my-company.com/realms/myrealm

    • Parent domain: my-company.com

    ❌ Cookies blocked as third party (Different Parent Domains)

    • App hosted at my-company.com

    • issuerUri:

      • https://accounts.google.com

      • https://xxxx.us.auth0.com

      • https://login.microsoftonline.com/xxx/v2.0

      • https://hydra.project-name.ory.cloud/

    • No common parent domain → Third-party cookies will be blocked.


    Google reCAPTCHA

    While reCAPTCHA is not directly related to oidc-spa, its cookies are set on the login page, outside of your app. Since this is a related concern, you can find more details here:

    import { createOidc } from "oidc-spa";
    import { createMockOidc } from "oidc-spa/mock";
    import { z } from "zod";
    
    const decodedIdTokenSchema = z.object({
        sub: z.string(),
        preferred_username: z.string()
    });
    
    const oidc = !import.meta.env.VITE_OIDC_ISSUER
        ? await createMockOidc({
              isUserInitiallyLoggedIn: false,
              // This is only so we know where to redirect when 
              // you call `logout({ redirectTo: "home" })`
              homeUrl: import.meata.env.BASE_URL,
              mockedTokens: {
                  decodedIdToken: {
                      sub: "123",
                      preferred_username: "john doe"
                  } satisfies z.infer<typeof decodedIdTokenSchema>
              }
          })
        : await createOidc({
              issuerUri: import.meta.env.VITE_OIDC_ISSUER,
              clientId: import.meta.env.VITE_OIDC_CLIENT_ID,
              homeUrl: import.meta.env.BASE_URL,
              decodedIdTokenSchema
          });

    Full-Stack with Node REST API

    A full-stack example covering both the backend and frontend

    This is a kitchen think example with the following stack:

    • Vite

    • TanStack Router - File Based Routing

    • A Todos Rest API implemented with Node and Hono

    The app is live here:

    The frontend (Vite project):

    The backend (Node Todos App REST API):

    User Account Management

    In this section we assume you are using Keycloak. If you are using another authentication server you'll have to addapt the queryParameter provided.

    When your user is logged in, you can provide a link to redirect to Keycloak so they can manage their account.

    There is thee main actions:

    • UPDATE_PASSWORD: Enables the user to change their password.

    React Router

    The example setup is live here:

    Run it locally with:

    This is for setting for integrating oidc-spa with react-router in .

    Enabling SPA mode

    As of today, to use oidc-spa you need to .

    Other OIDC Provider

    If you are using an OIDC provider other than the ones for which we have , follow these general instructions to configure your OIDC provider.

    Creating the Client Application

    • Create a Public OpenID Connect client.

    const { unsubscribeFromAutoLogoutCountdown } = oidc.subscribeToAutoLogoutCountdown(
      ({ secondsLeft }) => {
        if( secondsLeft === undefined ){
          console.log("Countdown reset, the user moved");
          return;
        }
        if( secondsLeft > 60 ){
          return;
        }
        console.log(`${secondsLeft} before auto logout`)
      }
    );
    src/oidc.ts
    import { createReactOidc } from "oidc-spa/react";
    import { createMockReactOidc } from "oidc-spa/mock/react";
    import { z } from "zod";
    
    const decodedIdTokenSchema = z.object({
        sub: z.string(),
        preferred_username: z.string()
    });
    
    const publicUrl = import.meta.env.BASE_URL;
    
    export const { OidcProvider, useOidc, getOidc } =
        !import.meta.env.VITE_OIDC_ISSUER ?
            createMockReactOidc({
                isUserInitiallyLoggedIn: false,
                // This is only so we know where to redirect when 
                // you call `logout({ redirectTo: "home" })`
                homeUrl: import.meta.env.BASE_URL,
                mockedTokens: {
                    decodedIdToken: {
                        sub: "123",
                        preferred_username: "john doe"
                    } satisfies z.infer<typeof decodedIdTokenSchema>
                }
            }) :
            createReactOidc({
                issuerUri: import.meta.env.VITE_OIDC_ISSUER,
                clientId: import.meta.env.VITE_OIDC_CLIENT_ID,
                homeUrl: import.meta.env.BASE_URL,
                decodedIdTokenSchema
            });
    })
    .catch((error: OidcInitializationError) => {
    // Handle potential initialization errors
    // In this mode, falling back to an unauthenticated state is not an option.
    if (!error.isAuthServerLikelyDown) {
    // This indicates a misconfiguration in your OIDC server.
    throw error;
    }
    alert("Sorry, our authentication server is down. Please try again later.");
    return new Promise<never>(() => {}); // Prevent further execution
    });
    :
    true
    ,
    // postLoginRedirectUrl: "/dashboard"
    });
    ({ routeTree });
    ReactDOM.createRoot(document.getElementById("root")!).render(
    <React.StrictMode>
    <OidcProvider
    ErrorFallback={({ initializationError }) => (
    <h1 style={{ color: "red" }}>
    {initializationError.isAuthServerLikelyDown ? (
    <>Sorry, our authentication server is currently down. Please try again later.</>
    ) : (
    // Debugging: Use initializationError.message for details.
    // This is an issue on your end and should not be shown to users.
    <>Unexpected authentication error.</>
    )}
    </h1>
    )}
    >
    <RouterProvider router={router} />
    </OidcProvider>
    </React.StrictMode>
    );

    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/

    • 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:

    Then your issuerUri is:

    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.

    • Microsoft Entra ID requires you to "register an application" and specify a scope.

    a specific guide
    https://XXX/.well-known/openid-configuration
    https://XXX
    idleSessionLifetimeInSeconds: 300 // 5 minutes
    //autoLogoutParams: { redirectTo: "current page" } // Default
    //autoLogoutParams: { redirectTo: "home" }
    //autoLogoutParams: { redirectTo: "specific url", url: "/a-page" }
    });
    // 5 minuts
    //autoLogoutParams: { redirectTo: "current page" } // Default
    //autoLogoutParams: { redirectTo: "home" }
    //autoLogoutParams: { redirectTo: "specific url", url: "/a-page" }
    });

    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.

  • Let's, as an example, how you would implement an update password button:

    import { createOidc } from "oidc-spa";
    import { parseKeycloakIssuerUri } from "oidc-spa/tools/parseKeycloakIssuerUri";
    
    const oidc = await createOidc({ ... });
    
    if( oidc.isUserLoggedIn ){
    
    
    oidc.client.ts

    Make sure you create a app/oidc.client.ts file, (instead of app/oidc.ts).

    Setting up the entrypoint

    Create thoses two files:

    Working with loaders

    If your whole app requires user to be authenticated (autoLogin: true) you can skip this section.

    The default approach when you want to enforce that the user be logged in when accesing a given route is to wrap the component into withLoginEnforced(), example:

    This approach is framework agnostic and always works however, you might want to use the loaders to doload the data, for that you would use enforceLogin() istead of withLoginEnforced:

    Running the example

    The example setup is live here: https://example-react-router-framework.oidc-spa.dev/

    Run it locally with:

    https://example-react-router.oidc-spa.dev/
    Framework Mode
    enable SPA mode
    npx degit https://github.com/keycloakify/oidc-spa/examples/react-router oidc-spa-react-router
    cd oidc-spa-react-router
    cp .env.local.sample .env.local
    yarn
    yarn dev

    Keycloak

    Getting the issuerUri and clientId

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

    issuerUri

    In Keycloak, the OIDC issuer URI follows this format:

    https://<KC_DOMAIN><KC_RELATIVE_PATH>/realms/<REALM_NAME>

    • <KC_DOMAIN>: The domain where your Keycloak server is hosted (e.g., auth.my-company.com).

    • <KC_RELATIVE_PATH>: 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

    clientId

    The clientId is usually something like 'myapp'. Follow these steps to create a suitable client for your SPA:

    1. Open https://<KC_DOMAIN><KC_RELATIVE_PATH>/admin/master/console.

    2. Log in as an administrator.

    3. Select your realm from the top-left dropdown.

    4. In the left panel, click


    Session Lifespan Configuration

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

    This configuration does not affect the access token lifetime (default: 5 minutes). It controls how long Keycloak keeps the session active.

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

    For security-critical apps, users should log in each visit and be logged out after inactivity.

    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 your realm.

      • Navigate to Realm Settings → Login.

      • Set "Remember Me" to


    🛍️ 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 your realm.

      • Navigate to Realm Settings → Login.

      • Set "Remember Me" to


    🗑️ Allowing Users to Delete Their Own Accounts

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

    If you implement a , 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


    Testing the Setup

    To test your configuration:

    Microsoft Entra ID

    Formerly Azure Active Directory

    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—you need to configure a custom scope.

    Steps to Configure a Custom Scope

    1. Go to .

    2. In the left panel, select "Microsoft Entra ID".

    3. Navigate to "Manage > App Registrations".

    4. Click "New Registration".

    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 .


    Registering Your Application

    1. Go to .

    2. In the left panel, select "Microsoft Entra ID".

    3. Navigate to "Manage > App Registrations".

    4. Click "New Registration".

    These are required to configure oidc-spa.


    Configuring oidc-spa


    Testing the Setup

    To test your configuration:

    import { parseKeycloakIssuerUri } from "oidc-spa/tools/parseKeycloakIssuerUri";
    
    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>
                )}
                <a 
                    href={parseKeycloakIssuerUri(params.issuerUri)!.getAccountUrl({
                        clientId,
                        backToAppFromAccountUrl: location.href
                    })}
                >
                        My Account
                </a>
            </>
        );
    }
    
    react-router.config.ts
    import type { Config } from "@react-router/dev/config";
    
    export default {
        // Config options...
        // Server-side render by default, to enable SPA mode set this to `false`
        ssr: false
    } satisfies Config;
    pages/invoices.tsx
    import { useState, useEffect } from "react";
    import { withLoginEnforced, fetchWithAuth } from "../oidc.client";
    
    const Invoices = withLoginEnforced(
        () => {
            const [invoices, setInvoices] = useState<Invoice[] | undefined>(undefined);
    
            useEffect(() => {
                fetchWithAuth("/api/invoices")
                    .then(r => r.json())
                    .then(setInvoices);
            }, []);
    
            if (invoices === undefined) {
                return <div>Loading invoices...</div>;
            }
    
            return (
                <div>
                    {invoices.map(invoice => (
                        <div key={invoice.id}>{invoice.amount}</div>
                    ))}
                </div>
            );
        },
        {
            onRedirecting: () => <div>Redirecting to login...</div>
        }
    );
    
    export default Invoices;
    pages/invoices.tsx
    import { enforceLogin, fetchWithAuth } from "../oidc.client";
    import type { Route } from "./+types/invoices";
    import { useLoaderData } from "react-router";
    
    export async function clientLoader(params: Route.ClientLoaderArgs) {
        await enforceLogin(params);
        // If we are here, the user is logged in.
        const invoices = await fetchWithAuth("/api/invoices").then(r => r.json());
        return invoices;
    }
    
    export function HydrateFallback() {
        return <div>Loading invoices...</div>;
    }
    
    export default function Invoices() {
        const invoices = useLoaderData<typeof clientLoader>();
    
        return (
            <div>
                {invoices.map(invoice => (
                    <div key={invoice.id}>{invoice.amount}</div>
                ))}
            </div>
        );
    }
    npx degit https://github.com/keycloakify/oidc-spa/examples/react-router-framework oidc-spa-react-router
    cd oidc-spa-react-router
    cp .env.local.sample .env.local
    yarn
    yarn dev
    const { ... } = createOidc({
        issuerUri: "...",
        clientId: "...",
        // ...
    });
    // 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}`
    });
    https://github.com/keycloakify/oidc-spa/tree/main/examples/tanstack-router-file-basedgithub.com
    Warning: this video has been recorded for oidc v5, the API has changed a little bit.
    <REALM_NAME>: 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://<KC_DOMAIN><KC_RELATIVE_PATH>/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 a name, and save.

    Clients
    .
  • Click Create Client.

  • Enter a Client ID, for example, myapp, and click Next.

  • Ensure Client Authentication is off, and Standard Flow is enabled. Click Next.

  • Set two Valid Redirect URIs, ensure both URLs end with /:

    • https://<APP_DOMAIN><BASE_URL>

    • http://localhost:<DEV_PORT><BASE_URL>

    • Parameters:

      • <APP_DOMAIN>: Examples: https://my-company.com or https://app.my-company.com. 🔹 For beter performances ensure <APP_DOMAIN> and <KC_DOMAIN> share the same root domain (my-company.com). See .

      • <BASE_URL>: Examples: "/" or "/dashboard/".

  • Click Save, and you're done! 🎉

  • Off
    .
  • 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).

  • Optionally, display a logout countdown before automatic logout:

  • On
    .
  • 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.

  • client
    , select
    Delete Account
    , and assign it.
    Auto Logout
    delete account button

    Enter "My App - API" as the name, then click Register.

  • Set Supported Account Type to Accounts in this organization.

  • In the left menu, go to "Manage > Expose API".

  • Click "Add a scope".

  • 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

  • Enter "My App" as the display name (replace with your actual app name).

  • Set Supported Account Type to Accounts in this organization.

  • Click Register.

  • Click "Add a Redirect URI".

  • Click "Add Platform" > "Single-Page Application".

  • 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)

  • Ensure "Access Token" and "ID Token" are checked.

  • Click Save.

  • In the left panel, go to "API Permissions".

  • Click "Add a permission".

  • Click "APIs My Organization Uses".

  • Select "My App - API".

  • Check "access_as_user", then click "Add permission".

  • In the left panel, click "Overview" and copy:

    • Application (client) ID

    • Directory (tenant) ID

  • Microsoft Azure Portal
    Web API documentation
    Microsoft Azure Portal

    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:

    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.

    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

    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

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

    Testing

    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 implementations of the Authorization Code Flow:

    • Authorization Code Flow: Requires a backend to handle the token exchange, as a client secret is needed to obtain tokens securely.

    • Authorization Code Flow + PKCE: Introduces an additional verification step, removing the need for a client secret, allowing secure token exchange directly from the client application.

    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.


    2. Authorization Code Flow + PKCE (Used by oidc-spa)

    The standard Authorization Code Flow alone is insufficient for securely exchanging tokens directly from the frontend, 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.

    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 .

    Advantages and Trade-offs of Implementing Token Exchange on the Frontend

    ✅ No persistent token storage – There’s no need to store user tokens in a backend database. The OIDC provider itself acts as the session store, meaning you only need to focus on securely deploying your OIDC server (if self-hosting).

    ✅ Fewer moving parts – Everything happens between oidc-spa and the OIDC provider, reducing the chances of misconfiguration. Even if you're not entirely confident in your setup, as long as it works, you’ve implemented it correctly.

    However, in this mode, tokens are exposed to the JavaScript client code, unlike when the token exchange is performed on the backend. This introduces a potential risk: XSS attacks, where malicious code running on your website could attempt to steal tokens.


    How oidc-spa Mitigates the Risks of Token Exposure

    oidc-spa implements several security measures to minimize the risk of token theft, even in the event of an XSS attack (malicious JavaScript running in your frontend).

    Note: This is a work in progress—some of these measures are not yet fully implemented, but they are actively being developed.

    To fully benefit from these protections, ensure oidc-spa is the first JavaScript code that runs on your website by following .

    Security Measures:

    • No persistent token storage – Tokens are never stored in localStorage or sessionStorage. Instead, they are kept in scoped variables that are inaccessible to the global scope.

    • Preventing fetch and XMLHttpRequest monkey patching – APIs that caries access tokens are frozen to prevent malicious code from overriding it and capturing tokens.

    Opinionated Conclusion

    PKCE is a widely adopted open standard, supported by all major OIDC providers, and provides strong security guarantees without requiring a backend.

    While a backend-based token exchange is theoretically more secure, in practice, it introduces additional attack surfaces and operational complexity. Every extra moving part is a potential point of failure or misconfiguration, and securing a backend against threats like token leakage, improper session management, and server-side vulnerabilities is a non-trivial task.

    With the security measures implemented in oidc-spa, Authorization Code Flow + PKCE is not just the simpler approach—it is arguably the safer one in real-world scenarios. By eliminating the backend entirely, it reduces the risk of misconfiguration and ensures that authentication security is handled directly by the OIDC provider, which is purpose-built for this task.

    Read more:

    Error Management

    Gracefully handle authentication issues

    What happens if the OIDC server is down, or if your OIDC server isn't properly configured?

    By default, if you don't have autoLogin enabled, when there is an error with the OIDC initialization your website will load with the user unauthenticated.

    This allows the user to at least access parts of the application that do not require authentication. When the user clicks on the login button (triggering the login() function), a browser alert is displayed, indicating that authentication is currently unavailable, and no further action is taken.

    You can customize this behavior. An initializationError object is present on the oidc object if an error occurred.

    import { createOidc } from "oidc-spa";
    
    const oidc = await createOidc(...);
    
    if( !oidc.isUserLoggedIn ){
        // If the used is logged in we had no initialization error.
        return;
    
    GitHub - keycloakify/keycloakify-starter at direct_impersonationGitHub
    Managed Keycloak Hosting and Enterprise Keycloak Supportphasetwo.io
    Keycloak community contributors of popular extensions providing free and dedicated Keycloak hosting and enterprise Keycloak support to businesses of all sizes.
    oidc-spa/examples/tanstack-router/src/oidc.tsx at a5dbcca428721c4ea4ed1fd36dc07e80fc9d2cb0 · keycloakify/oidc-spaGitHub
    Insee SPA Startervite-insee-starter.demo-domain.ovh

    Web API

    The primary usecase for a library like oidc-spa is to use it to authenticate against a REST, tRPC, or Websocket API.

    Client Side

    Let's see a very basic REST API example:

    Initialize oidc-spa and expose the oidc instance as a promise:

    Create a REST API Client that adds the OIDC Access Token as Autorization header to every HTTP request:

    oidc-spa/examples/tanstack-router/src/router/AutoLogoutCountdown.tsx at v6 · keycloakify/oidc-spaGitHub
    The demo app with a short SSO Session Idle
    Why do I get reCaptcha warnings? | FAQ | Keycloakifydocs.keycloakify.dev
    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
    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
    });
    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
    });
    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
    Logo
    }
    if( oidc.initializationError ){
    // This help you discriminate configuration errors
    // and error due to the server being temporarely down.
    console.log(oidc.initializationError.isAuthServerLikelyDown);
    const handleLoginClick = ()=> {
    if( oidc.initializationError ){
    alert(`Can't login now, try again later ${oidc.initializationError.message}`);
    return;
    }
    oidc.login(...);
    };
    }
    Create a REST API Client that adds the OIDC Access Token as Autorization header to every HTTP request:

    Using your REST API client in your REACT components:

    This example is purposefully very basic to minimize noise but in your App you might want to consider using solutions like tRPC (if you have JS backend) and TanStack Query.

    Server Side

    If you're implementing a JavaScript Backend (Node/Deno/webworker) oidc-spa also exposes an utility to help you validate and decode the access token that your client sends in the authorization header. Granted, this is fully optional feel free to use anything else. Let's assume we have a Node.js REST API build with Express or Hono. You can create an oidc file as such:

    Then you can enforce that some endpoints of your API requires the user to be authenticated, in this example we use Hono:

    Testable example

    src/oidc.ts
    import { createOidc } from "oidc-spa";
    
    export const prOidc = createOidc({/* ... */});
    src/api.ts
    import axios from "axios";
    import { prOidc } from "./oidc";
    
    type Api = {
        getTodos: () => Promise<{ id: number; title: string; }[]>;
        addTodo: (todo: { title: string; }) => Promise<void>;
    };
    
    const axiosInstance = axios.create({ baseURL: import.meta.env.API_URL });
    
    axiosInstance.interceptors.request.use(async config => {
    
        const oidc= await prOidc;
    
        if( !oidc.isUserLoggedIn ){
            throw new Error("We made a logic error: If the user isn't logged in we shouldn't be making request to an API endpoint that requires authentication");
        }
        
        // 99.9% of the times you'll get the token imediately.
        // The 0.1% is after the computer wakes up from sleep.
        const { accessToken } = await oidc.getTokens_next();
        
        config.headers.Authorization = `Bearer ${accessToken}`;
    
        return config;
    
    });
    
    export const api: Api = {
        getTodo: ()=> axiosInstance.get("/todo").then(response => response.data),
        addTodo: todo => axiosInstance.post("/todo", todo).then(response => response.data)
    };
    Full-Stack with Node REST API
    import { useOidc } from "oidc";
    import { useEffect } from "react";
    
    function LoginButton() {
    
        const { isUserLoggedIn, login, logout, initializationError } = useOidc();
    
        useEffect(() => {
            if (initializationError) {
                // This help you discriminate configuration errors
                // and error due to the server being temporarely down.
                console.log(initializationError.isAuthServerLikelyDown);
            
                // This is a debug message that tells you what's wrong
                // with your configuration and how to fix it.  
                // (this is not something you want to display to the user)
                console.log(initializationError.message);
            }
        }, []);
    
        if (isUserLoggedIn) {
            return <button onClick={()=> logout({ redirectTo: "home" })}>Logout</button>;
        }
    
        return (
            <button onClick={() => {
    
                if (initializationError) {
                    alert("Can't login now, try again later")
                    return;
                }
    
                login({ ... });
    
            }}>
                Login
            </button>
        );
    
    }
    src/oidc.ts
    import { createReactOidc } from "oidc-spa/react";
    
    export const { 
        OidcProvider, 
        useOidc,
        getOidc
    } = createReactOidc(/* ... */);
    src/api.ts
    import axios from "axios";
    import { getOidc } from "./oidc";
    
    type Api = {
        getTodos: () => Promise<{ id: number; title: string; }[]>;
        addTodo: (todo: { title: string; }) => Promise<void>;
    };
    
    const axiosInstance = axios.create({ baseURL: import.meta.env.API_URL });
    
    axiosInstance.interceptors.request.use(async config => {
    
        const oidc= await getOidc();
    
        if( !oidc.isUserLoggedIn ){
            throw new Error("We made a logic error: The user should be logged in at this point");
        }
        
        const { accessToken } = await oidc.getTokens();
        
        config.headers.Authorization = `Bearer ${accessToken}`;
    
        return config;
    
    });
    
    export const api: Api = {
        getTodo: ()=> axiosInstance.get("/todo").then(response => response.data),
        addTodo: todo => axiosInstance.post("/todo", todo).then(response => response.data)
    };
    import { api } from "../api";
    
    type Todo= {
        id: number; 
        title: string;
    };
    
    function UserTodos(){
    
        const [todos, setTodos] = useState<Todo[] | undefined>(undefined);
    
        useEffect(
            ()=> {
                api.getTodos().then(todos => setTodos(todos));
            },
            []
        );
    
        if(todos === undefined){
            return <>Loading your todos items ⌛️</>
        }
    
        return (
            <ul>
                {todos.map(todo => (
                    <li key={todo.id}>{todo.title}</li>
                ))}
            </ul>
        );
    
    }
    src/oidc.ts
    import { createOidcBackend } from "oidc-spa/backend";
    import { z } from "zod";
    import { HTTPException } from "hono/http-exception";
    
    const zDecodedAccessToken = z.object({
        sub: z.string(),
        aud: z.union([z.string(), z.array(z.string())]),
        realm_access: z.object({
            roles: z.array(z.string())
        })
        // Some other info you might want to read from the accessToken, example:
        // preferred_username: z.string()
    });
    
    export type DecodedAccessToken = z.infer<typeof zDecodedAccessToken>;
    
    export async function createDecodeAccessToken(params: { 
        issuerUri: string;
        audience: string 
    }) {
        const { issuerUri, audience } = params;
    
        const { verifyAndDecodeAccessToken } = await createOidcBackend({
            issuerUri,
            decodedAccessTokenSchema: zDecodedAccessToken
        });
    
        function decodeAccessToken(params: {
            authorizationHeaderValue: string | undefined;
            requiredRole?: string;
        }): DecodedAccessToken {
            const { authorizationHeaderValue, requiredRole } = params;
    
            if (authorizationHeaderValue === undefined) {
                throw new HTTPException(401);
            }
    
            const result = verifyAndDecodeAccessToken({
                accessToken: authorizationHeaderValue.replace(/^Bearer /, "")
            });
    
            if (!result.isValid) {
                switch (result.errorCase) {
                    case "does not respect schema":
                        throw new Error(`The access token does not respect the schema ${result.errorMessage}`);
                    case "invalid signature":
                    case "expired":
                        throw new HTTPException(401);
                }
            }
    
            const { decodedAccessToken } = result;
    
            if (requiredRole !== undefined && !decodedAccessToken.realm_access.roles.includes(requiredRole)) {
                throw new HTTPException(401);
            }
    
            {
                const { aud } = decodedAccessToken;
    
                const aud_array = typeof aud === "string" ? [aud] : aud;
    
                if (!aud_array.includes(audience)) {
                    throw new HTTPException(401);
                }
            }
    
            return decodedAccessToken;
        }
    
        return { decodeAccessToken };
    }
    import { z, createRoute, OpenAPIHono } from "@hono/zod-openapi";
    import { serve } from "@hono/node-server"
    import { HTTPException } from "hono/http-exception";
    import { getUserTodoStore } from "./todo";
    import { createDecodeAccessToken } from "./oidc";
    
    (async function main() {
    
        const { decodeAccessToken } = await createDecodeAccessToken({
            // Here example with Keycloak but it work the same with 
            // any provider as long as the access token is a JWT.
            issuerUri: "https://auth.my-company.com/realms/myrealm",
            audience: "account" // default audience in Keycloak
        });
    
        const app = new OpenAPIHono();
    
        {
    
            const route = createRoute({
                method: 'get',
                path: '/todos',
                responses: {/* ... */}
            });
    
            app.openapi(route, async c => {
    
                const decodedAccessToken = decodeAccessToken({
                    authorizationHeaderValue: c.req.header("Authorization")
                });
    
                const todos = await getUserTodoStore(decodedAccessToken.sub).getAll();
    
                return c.json(todos);
    
            });
    
        }
    
        const port = parseInt(process.env.PORT);
    
        serve({
            fetch: app.fetch,
            port
        })
    
        console.log(`\nServer running. OpenAPI documentation available at http://localhost:${port}/doc`)
    
    })();

    <DEV_PORT>: Example: 5173 (default for Vite's dev server, adapt to your setup).

    end of third party cookies

    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.

  • Set the Authorized JavaScript Origins to match the origins of your redirect URIs.

  • . This is purely a poor API design decision on Google's part.
    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.

    Web API

    The frontend communicates with the backend, which retrieves and attaches access tokens to API requests using the session identifier stored in the cookie.

  • Tokens remain in memory only—oidc-spa does not store them in localStorage, sessionStorage, or a backend database.

  • When the user refreshes the page or revisits the app, oidc-spa restores the session by querying the OIDC provider in the background.

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

  • If the session has expired, the user is redirected to the login page when accessing a protected area.

  • (WIP) Securing silent sign-in responses – Even if an attacker intercepts the authorization response from a silent sign-in performed in an iframe, it will be asymmetrically encrypted, making it unusable.
  • (WIP) Secure transfer of the authorization response after front-channel login – The authorization response, which is temporarily stored in session storage during the redirect process, will be cleared and moved to memory before any other code runs.

  • third-party cookies
    this setup
    Logo
    Cloud identity and acces managementcloud-iam.com
    Keycloak as a Service. Use code 'keycloakify5' at checkout for a 5% discount.

    Auth0

    This guide explains how to configure Auth0 to obtain the necessary parameters for setting up oidc-spa.

    Creating Your Application

    1. Navigate to .

    https://github.com/keycloakify/oidc-spa/tree/main/examples/react-routergithub.com
    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
    });
    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
    });
    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
    oidc-spa with Keycloak
    In the left panel, go to Applications → Applications.
  • Click Create Application.

  • Select Single Page Application as the application type.

  • Navigate to the Settings tab to find the Domain and Client ID.

  • Scroll to the Application URIs section. Set two Allowed Callback URLs, ensure both URLs end with /:

    • https://<APP_DOMAIN><BASE_URL>

    • http://localhost:<DEV_PORT><BASE_URL>

    • Parameters:

      • <APP_DOMAIN>: Examples: https://my-company.com or https://app.my-company.com. 🔹 For beter performances ensure <APP_DOMAIN> and <KC_DOMAIN> share the same root domain (my-company.com). See .

      • <BASE_URL>: Examples: "/" or "/dashboard/".

  • Allowed Logout URLs: Copy paste what you put into Allowed Callback URLs

  • Allowed Web Origins: The origins of the Callback URLs

  • Click Save Changes

  • Creating an API

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

    1. Navigate to Auth0 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 for more info.

      • Click Save.

    (Optional) Configuring a Custom Domain

    It is highly recommended to set up a custom domain in Auth0 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.

    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 for more details.

    Once configured, use your custom domain as the issuerUri:

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

    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.

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

    You can enhance user experience by displaying a countdown warning before logout:


    Testing the Setup

    To test your configuration:

    Auth0 Dashboard
    Auto Logout
    Keycloak Consulting - Configuration, Deployments and morewww.zone2.tech
    Keycloak Consulting Services - Your partner in Keycloak deployment, configuration, and extension development .
    GitHub - InseeFrLab/vite-insee-starter: A template project for SPAsGitHub
    GitHub - InseeFrLab/todo-rest-api: Todo app rest API with OIDC and OpenAPIGitHub
    https://github.com/keycloakify/oidc-spa/blob/main/examples/react-router-framework/app/entry.client.lazy.tsxgithub.com
    app/entry.client.lazy.tsx
    oidc-spa/examples/react-router-framework at main · keycloakify/oidc-spaGitHub
    https://github.com/keycloakify/oidc-spa/blob/main/examples/react-router-framework/app/oidc.client.tsgithub.com
    Example of oidc.client.ts file
    https://github.com/keycloakify/oidc-spa/blob/main/examples/react-router-framework/app/entry.client.tsxgithub.com
    app/entry.client.tsx

    Discord Server

    Feeling a bit lost? Have a question? A feature request? Reach out on Discrord!

    const { ... } = createOidc({
        // Referred to as "Domain" in Auth0:
        issuerUri: "dev-r2h8076n6dns3d4y.us.auth0.com",
        clientId: "DzXSmwQS7oSTQGLbafhrPXYLT0mOMyZD",
    });
    const { ... } = createOidc({
        issuerUri: "dev-r2h8076n6dns3d4y.us.auth0.com",
        clientId: "DzXSmwQS7oSTQGLbafhrPXYLT0mOMyZD",
        extraQueryParams: {
           audience: "https://app.my-company.com/api"
        }
    });
     const { ... } = createOidc({
    -    issuerUri: "dev-r2h8076n6dns3d4y.us.auth0.com",
    +    issuerUri: "auth.my-company.com",
         clientId: "DzXSmwQS7oSTQGLbafhrPXYLT0mOMyZD",
         extraQueryParams: {
             audience: "https://app.my-company.com/api"
         }
     });
    const { ... } = createOidc({
        issuerUri: "auth.my-company.com",
        clientId: "DzXSmwQS7oSTQGLbafhrPXYLT0mOMyZD",
        extraQueryParams: {
           audience: "https://app.my-company.com/api"
        },
        idleSessionLifetimeInSeconds: 300
    });
    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

    <DEV_PORT>: Example: 5173 (default for Vite's dev server, adapt to your setup).

    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.

  • Click Save.

  • end of third party cookies
    the web API page
    the end of third-party cookie page
    Logo
    Logo
    Logo

    Basic Usage

    Let's get your App authenticated!

    Before getting started, you need to get a hold of the few parameters required to connect to your OIDC provider. Find instruction on how to configure your OIDC provider on the following documentation page:

    Provider configuration
    import { createOidc } from "oidc-spa";
    import { z } from "zod";
    
    const oidc = await createOidc({
        issuerUri: "https://auth.your-domain.net/realms/myrealm",
        clientId: 
    

    The way you use oidc-spa differs slightly depending on the routing library you’re using (e.g., React Router or TanStack Router). We provide specific setup guides for each, but we recommend starting with the fictional example below to understand how the library works in isolation, without any routing-related distractions.

    Note: In this example, some pages can be accessed without requiring the user to be authenticated. If you're building something like an admin panel or a dashboard where authentication is always required, simply set autoLogin: true.

    Now that you got the idea you can follow up with the specific setup guides for different stacks:

    Setup Guides
    src/
    ├── components/
    │   └── Header.tsx
    ├── pages/
    │   ├── Home.tsx
    │   ├── Account.tsx
    │   ├── Orders.tsx
    ├── App.tsx
    └── oidc.tsx
    src/oidc.ts
    import { createReactOidc } from "oidc-spa/react";
    import { z } from "zod";
    
    export const { OidcProvider, useOidc, getOidc, withLoginEnforced, enforceLogin } =
        createReactOidc(async () => ({
            issuerUri: "https://auth.your-domain.net/realms/myrealm",
            clientId: "myclient",
            /**
             * Vite:  `homeUrl: import.meta.env.BASE_URL`
             * CRA:   `homeUrl: process.env.PUBLIC_URL`
             * Other: `homeUrl: "/"` (Usually, can be something like "/dashboard")
             */
            homeUrl: import.meta.env.BASE_URL,
            //scopes: ["profile", "email", "api://my-app/access_as_user"],
            extraQueryParams: () => ({
                ui_locales: "en" // Keycloak login/register page language
                //audience: "https://my-app.my-company.com/api"
            }),
            decodedIdTokenSchema: z.object({
                preferred_username: z.string(),
                name: z.string()
                //email: z.string().email().optional()
            })
        }));
    
    export const fetchWithAuth: typeof fetch = async (
        input,
        init
    ) => {
        const oidc = await getOidc();
        
        if (oidc.isUserLoggedIn) {
            const { accessToken } = await oidc.getTokens();
    
            (init ??= {}).headers = {
                ...init.headers,
                Authorization: `Bearer ${accessToken}`
            };
        }
    
        return fetch(input, init);
    };
    src/App.tsx
    import { Suspense, lazy } from "react";
    import Header from "./components/Header";
    const HomePage = lazy(() => import("./pages/Home"));
    const OrderPage = lazy(() => import("./pages/Orders"));
    const AccountPage = lazy(() => import("./pages/Account"));
    
    export default function App() {
        const route = useRoute();
    
        return (
            <OidcProvider
              //fallback={<h1>Checking authentication ⌛️</h1>}
            >
                <Header />
                <main>
                    <Suspense>
                        {route === "/home" && <HomePage />}
                        {route === "/orders" && <OrderPage />}
                        {route === "/account" && <AccountPage />}
                    </Suspense>
                </main>
            </OidcProvider>
        );
    }
    src/components/Header.tsx
    import { useOidc } from "../oidc";
    
    export default function Header() {
        const { isUserLoggedIn } = useOidc();
    
        return (
            <header>
                <nav>
                    <Link to="/home">Home</Link>
                    <Link to="/orders">Orders</Link>
                </nav>
                {isUserLoggedIn ? (
                    <AuthButtonsLoggedIn />
                ) : (
                    <AuthButtonsNotLoggedIn />
                )}
            </header>
        );
    }
    
    function AuthButtonsLoggedIn() {
        const { decodedIdToken, logout } = useOidc({ assert: "user logged in" });
    
        return (
            <div>
                <span>Logged in as {decodedIdToken.preferred_username}</span>
                <Link to="/account">Account</Link>
                <button onClick={() => logout({ redirectTo: "home" })}>
                    Logout
                </button>
            </div>
        );
    }
    
    function AuthButtonsNotLoggedIn() {
        const { login } = useOidc({ assert: "user not logged in" });
    
        return (
            <div>
                <button onClick={() => login()}>Login</button>
                <button
                    onClick={() =>
                        login({
                            // Keycloak:
                            transformUrlBeforeRedirect: url => {
                                const urlObj = new URL(url);
                                urlObj.pathname = urlObj.pathname.replace(
                                    /\/auth$/,
                                    "/registrations"
                                );
                                return urlObj.href;
                            }
                            // Auth0:
                            // extraQueryParams: { screen_hint: "signup" }
                        })
                    }
                >
                    Register
                </button>
            </div>
        );
    }
    src/pages/Home.tsx
    import { useOidc } from "../oidc";
    
    export default function Page() {
        const { isUserLoggedIn, decodedIdToken } = useOidc();
    
        return (
            <h1>Welcome {isUserLoggedIn ? decodedIdToken.name : "guest"}!</h1>
        );
    }
    src/pages/Orders.tsx
    import { useEffect, useState } from "react";
    import { withLoginEnforced, fetchWithAuth } from "../oidc";
    
    type Order = {
        id: number;
        name: string;
    };
    
    // If this component is mounted and the user is not logged in
    // the user will be redirected to the login.  
    // If your routing library support loader you can use enforceLogin
    // instead of withLoginEnforced
    const Page = withLoginEnforced(() => {
        const [orders, setOrders] = useState<Order[] | undefined>(undefined);
    
        useEffect(() => {
            fetchWithAuth("https://api.your-domain.net/orders", {
                headers: {
                    "Content-Type": "application/json"
                }
            })
                .then(response => response.json())
                .then(orders => setOrders(orders));
        }, []);
    
        if (orders === undefined) {
            return <>Loading orders ⌛️</>;
        }
    
        return (
            <ul>
                {orders.map(order => (
                    <li key={order.id}>{order.name}</li>
                ))}
            </ul>
        );
    });
    
    export default Page;
    src/pages/Account.tsx
    import { useOidc, withLoginEnforced } from "../oidc";
    import { parseKeycloakIssuerUri } from "oidc-spa/tools/parseKeycloakIssuerUri";
    
    const Page = withLoginEnforced(() => {
        const {
            goToAuthServer,
            backFromAuthServer,
            params: { issuerUri, clientId }
        } = useOidc({ assert: "user logged in" });
    
        const keycloak = parseKeycloakIssuerUri(issuerUri);
    
        if (keycloak === undefined) {
            throw new Error(
                "We expect Keycloak to be the OIDC provider of this App"
            );
        }
    
        return (
            <div>
                <h1>Account</h1>
                <p>
                    <a
                        href={keycloak.getAccountUrl({
                            clientId,
                            backToAppFromAccountUrl: location.href,
                            locale: "en"
                        })}
                    >
                        Go to Keycloak Account Management Page
                    </a>
                </p>
                <p>
                    <button
                        onClick={() =>
                            goToAuthServer({
                                extraQueryParams: {
                                    kc_action: "UPDATE_PASSWORD"
                                }
                            })
                        }
                    >
                        Change My Password
                    </button>
                    {backFromAuthServer?.extraQueryParams.kc_action ===
                        "UPDATE_PASSWORD" && (
                        <span>
                            {backFromAuthServer.result.kc_action_status ===
                            "success"
                                ? "Password Updated!"
                                : "Password unchanged"}
                        </span>
                    )}
                </p>
                <p>
                    <button
                        onClick={() =>
                            goToAuthServer({
                                extraQueryParams: {
                                    kc_action: "UPDATE_PROFILE"
                                }
                            })
                        }
                    >
                        Update My Profile Information
                    </button>
                    {backFromAuthServer?.extraQueryParams.kc_action ===
                        "UPDATE_PROFILE" && (
                        <span>
                            {backFromAuthServer.result.kc_action_status ===
                            "success"
                                ? "Profile Updated!"
                                : "Profile unchanged"}
                        </span>
                    )}
                </p>
                <p>
                    <button
                        onClick={() =>
                            goToAuthServer({
                                extraQueryParams: {
                                    kc_action: "delete_account"
                                }
                            })
                        }
                    >
                        Delete My Account
                    </button>
                </p>
            </div>
        );
    });
    
    export default Page;
    "myclient"
    ,
    /**
    * Vite: `homeUrl: import.meta.env.BASE_URL`
    * CRA: `homeUrl: process.env.PUBLIC_URL`
    * Other: `homeUrl: "/"` (Usually, can be something like "/dashboard")
    */
    homeUrl: import.meta.env.BASE_URL,
    //scopes: ["profile", "email", "api://my-app/access_as_user"],
    extraQueryParams: () => ({
    ui_locales: "en" // Keycloak login/register page language
    //audience: "https://my-app.my-company.com/api"
    }),
    decodedIdTokenSchema: z.object({
    preferred_username: z.string(),
    name: z.string()
    //email: z.string().email().optional()
    })
    });
    if (!oidc.isUserLoggedIn) {
    // The user is not logged in.
    // We can call login() to redirect the user to the login/register page.
    // This return a promise that never resolve.
    oidc.login({
    /**
    * If you are calling login() in the callback of a click event
    * set this to false.
    */
    doesCurrentHrefRequiresAuth: false
    /**
    * Optionally, you can add some extra parameter
    * to be added on the login url.
    * (Can also be a parameter of createOidc `extraQueryParams: ()=> ({ ui_locales: "fr" })`)
    */
    //extraQueryParams: { kc_idp_hint: "google", ui_locales: "fr" }
    /**
    * You can allso set where to redirect the user after
    * successful login
    */
    // redirectUrl: "/dashboard"
    /**
    * Keycloak: You can also send the users directly to the register page
    * see: https://github.com/keycloakify/oidc-spa/blob/14a3777601c50fa69d1221495d77668e97443119/examples/tanstack-router-file-based/src/components/Header.tsx#L54-L66
    */
    });
    } else {
    // 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_next();
    fetch("https://api.your-domain.net/orders", {
    headers: {
    Authorization: `Bearer ${accessToken}`
    }
    })
    .then(response => response.json())
    .then(orders => console.log(orders));
    // To call when the user click on 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}`);
    }
    Logo
    Logo

    Tokens Renewal

    You don't need to do it. The token refresh is handled automatically for you, however you can manually trigger a token refresh.

    import { createOidc } from "oidc-spa";
    
    const prOidc = await createOidc({ ... });
    
    // Function to call when we want to renew the token
    export function renewTokens(){
    
       const
    

    Outside of a React Component:

    Authorization Code Flow with Proof Key for Code Exchange (PKCE)Auth0 Docs
    import { useOidc } from "./oidc";
    import { decodeJwt } from "oidc-spa/tools/decodeJwt";
    
    function DisplayAccessToken() {
        const { tokens, renewTokens } = useOidc({ assert: "user logged in" });
    
        if (tokens === undefined) {
            // NOTE: The tokens object is always initially undefined on first
            // render, the hook will imediately re-render automatically.
            return null;
        }
    
        return (
            <div>
                <h3>Access Token</h3>
                <pre>{JSON.stringify(decodeJwt(tokens.accessToken), null, 2)}</pre>
                <button onClick={() => 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      
                )}>Renew Tokens</button>
            </div>
        );
    }
    import { createReactOidc } from "oidc-spa/react";
    
    const prOidc = await createReactOidc({ ... });
    
    // 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.renewToken(
          // 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 { unsubscribe } = oidc.subscribeToTokensChange(tokens => {
           console.log("Token Renewed", tokens);
        });
        
        setTimeout(() => {
            // Call unsubscribe when you want to stop watching tokens change
            unsubscribe();
        }, 10_000);
    });
    oidc
    =
    await
    prOidc;
    if( !oidc.isUserLoggedIn ){
    throw new Error("Logical error");
    }
    oidc.renewToken(
    // 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 { unsubscribe } = oidc.subscribeToTokensChange(tokens => {
    console.log("Token Renewed", tokens);
    });
    setTimeout(() => {
    // Call unsubscribe when you want to stop watching tokens change
    unsubscribe();
    }, 10_000);
    });
    Logo
    Logo
    Join the Keycloakify Discord Server!Discord
    Logo
    Logo
    Logo
    Logo