arrow-left

Only this pageAll pages
gitbookPowered by GitBook
1 of 24

v5

Documentation

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Example setups

Loading...

Loading...

Resources

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Installation

circle-info

Before starting be aware that oidc-spa does not yet support Next.js projects.

If you are using Next the closer alternative is to use NextAuth.jsarrow-up-right (with the Keycloak adapterarrow-up-right if you are using Keycloak). You can refer to the phase two guidearrow-up-right.

If you're having issues don't hesitate to reach out on Discordarrow-up-right!

Let's install oidc-spaarrow-up-right in your project:

npm install oidc-spa

Create the silent-sso.htm file in your public directory:

chevron-rightDoing without the silent-sso.htm filehashtag

If for some reasons it's not fesable or practical for you to rely on the silent-sso.htm file it's ok, it will work without it.

Just make sure to

  • Set publicUrl to undefined

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)

hashtag
Configuring auto logout policy

This is a policy that is enforced on the identity server.

The auto logout is defined by the lifespan of the refresh token.

For example, if you're using Keycloak and you want an auto disconnect after 10 minutes of inactivity you would set the SSO Session Idle to 10 minutes. See .

Discord Server

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

TanStack Router

Vite + TypeScript + React + Tanstack Router

The example setup is live here:

Run it locally with:

Migration Guides

⬆️v4 -> v5chevron-right
when initializing oidc-spa.
  • Don't use logout({ redirectTo: "home" }) but explicitely tell where you want your users to be redirected after logout using logout({ redirectTo: "specific url", url: "/my-home" }) or use logout({ redirectTo: "current page" }).

  • yarn add oidc-spa
    pnpm add oidc-spa
    bun add oidc-spa
    public/silent-sso.htm
    <!doctype html>
    <html>
        <body>
            <script>
                parent.postMessage(location.href, location.origin);
            </script>
        </body>
    </html>
    If you can't configure your identity provider you can still enforce auto logout like so:
    import { createOidc } from "oidc-spa";
    
    import { createReactOidc } from "oidc-spa/react";
    

    Note that this parameter is marked as unsafe because what happens if the user closes the tab? He will be able to return a while back and still be logged in. oidc-spa can't enforce a security policy when it's not running. Only the identity server can.

    hashtag
    Displaying a coutdown timer before auto logout

    Keycloak configuration guide
    git clone https://github.com/keycloakify/oidc-spa
    mv oidc-spa/examples/tanstack-router-file-based oidc-spa-tanstack-router
    rm -rf oidc-spa
    cd oidc-spa-tanstack-router
    yarn
    yarn dev
    https://example-tanstack-router.oidc-spa.dev/arrow-up-right

    Disabeling token persistance

    By default, oidc-spa saves tokens in session storage, allowing users to skip the silent sign-in process when they reload your app (CTRL + R). However, token persistence can be disabled, and everything will still function as expected. The only difference is that reloading the app may take slightly longer.

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

    Globally Enforce Authentication

    If there is no part of your app that can be accessed without being logged it you can make oidc-spa automatically redirect your users to the login pages when they are not authenticated.

    Note that in this mode you don't have to check isUserLoggedIn (you know it's true), or useOidc({ assertUserLoggedIn: true })(you know that's the case).

    import { createOidc, OidcInitializationError } from "
    
    src/oidc.ts
    import { createReactOidc } from
    
    src/main.tsx
    
    import { OidcProvider }
    

    v4 -> v5

    Very little change in the API.

    • The public/silent-sso.html -> to public/silent-sso.htm (without the l)

    • Use getOidc() instead or prOidc (React API)

    If you are using the mock with the vanilla API (not React)

    React Router

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

    Run it locally with:

    git clone https://github.com/keycloakify/oidc-spa
    mv oidc-spa/examples/react-router oidc-spa-react-router
    rm -rf oidc-spa
    cd oidc-spa-react-router
    yarn
    yarn dev

    Tokens Renewal

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

    You can also track when the token are being refreshed:

    Or directly in your component:

    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:

    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`)
      }
    );
    const oidc = await createOidc({
    // ...
    doDisableTokenPersistence: true
    });
    export const {
    OidcProvider,
    useOidc
    } = createReactOidc({
    // ...
    doDisableTokenPersistence: true
    })
    oidc-spa
    "
    ;
    try{
    const oidc = await createOidc({
    // ...
    isAuthGloballyRequired: true,
    // Optional, the default value is: location.href (here)
    // postLoginRedirectUrl: "/dashboard"
    });
    }catch(error){
    const oidcInitializationError = error as OidcInitializationError;
    console.log(oidcInitializationError.message);
    console.log(oidcInitializationError.type); // "server down" | "bad configuration" | "unknown";
    }
    "
    oidc-spa/react
    "
    ;
    export const {
    OidcProvider,
    useOidc,
    prOidc
    } = createReactOidc({
    // ...
    isAuthGloballyRequired: true,
    // Optional, the default value is: location.href (here)
    // postLoginRedirectUrl: "/dashboard"
    });
    from
    "
    oidc
    "
    ;
    ReactDOM.createRoot(document.getElementById("root")!).render(
    <React.StrictMode>
    <OidcProvider
    ErrorFallback={({ initializationError })=>(
    <h1 style={{ color: "red" }}>
    An error occurred while initializing the OIDC client:
    {initializationError.message}
    {initializationError.type} /* "server down" | "bad configuration" | "unknown"; */
    </h1>
    )}
    >
    {/* ... */}
    </OidcProvider>
    </React.StrictMode>
    );
    import { createOidc } from "oidc-spa";
    
    const oidc = await createOidc({ ... });
    
    if( oidc.isUserLoggedIn ){
       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
       );
    }
    import { useOidc } from "oidc";
    import { useEffect } from "react";
    
    function MyComponent(){
    
      const { renewTokens } = useOidc({ assertUserLoggedIn: true });
      
      useEffect(()=> {
          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 createReactOidc
            // function. See: https://github.com/keycloakify/oidc-spa/blob/59b8db7db0b47c84e8f383a86677e88e884887cb/src/oidc.ts#L153-L163
          );
      }, []);
      
      return <>...</>;
    
    }
    import { createOidc } from "oidc-spa";
    
    const oidc = await createOidc({ ... });
    
    if( !oidc.isUserLoggedIn ){
        oidc.subscribeToTokensChange(() => {
            console.log("Tokens change", oidc.getTokens());
        });
    }
    src/oidc.ts
    import { createReactOidc } from "oidc-spa/react";
    
    export const {
        /* ... */
        getOidc
    } = createReactOidc({ /* ... */ });
    
    getOidc().then(oidc => {
    
        if( !oidc.isUserLoggedIn ){
            return;
        }
    
        oidc.subscribeToTokensChange(() => {
            console.log("Tokens change", oidc.getTokens());
        });
    
    });
    import { useOidc } from "./oidc";
    
    export function PotectedPage() {
        const { oidcTokens } = useOidc({ assertUserLoggedIn: true});
    
        useEffect(()=> {
    
            console.log("Tokens changed", oidcTokens);
    
        }, [oidcTokens]);
        
        // ...
    }
    src/oidc.ts
     import { createReactOidc } from "oidc-spa/react";
     
     export const {
         OidcProvider,
         useOidc,
    -    prOidc,
    +    getOidc
     } = createReactOidc({ });
    
    -prOidc.then(oidc => { /* ... */ });
    +getOidc().then(oidc => { /* ... */ });
     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 publicUrl= import.meta.env.BASE_URL;
    
     const oidc = !import.meta.env.VITE_OIDC_ISSUER
    -    ? await createMockOidc({
    +    ? await createMockOidc({
               isUserInitiallyLoggedIn: false,
               publicUrl,
               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,
               publicUrl
           });
    const oidc = await createOidc({
    // ...
    __unsafe_ssoSessionIdleSeconds: 10 * 60 // 10 minutes
    //autoLogoutParams: { redirectTo: "current page" } // Default
    //autoLogoutParams: { redirectTo: "home" }
    //autoLogoutParams: { redirectTo: "specific url", url: "/a-page" }
    });
    export const {
    OidcProvider,
    useOidc
    } = createReactOidc({
    // ...
    __unsafe_ssoSessionIdleSeconds: 10 * 60 // Ten minutes
    //autoLogoutParams: { redirectTo: "current page" } // Default
    //autoLogoutParams: { redirectTo: "home" }
    //autoLogoutParams: { redirectTo: "specific url", url: "/a-page" }
    });
    import { decodeJwt } from "oidc-spa/tools/decodeJwt";
    
    const { oidcTokens } = useOidc();
    
    const decodedAccessToken = decodeJwt(oidcTokens.accessToken);
    import { decodeJwt } from "oidc-spa/tools/decodeJwt";
    
    const decodedAccessToken = decodeJwt(oidc.getTokens().accessToken);

    User Account Management

    circle-info

    In this section we assume the reader is using Keycloak. If you are using another kind of 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.

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

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

    Basic Usage

    Let's get your App authenticated!

    circle-info

    In this guide, we assume that you have an OIDC-enabled authentication server in place, such as Keycloak.

    If you have not yet set up such a server, please refer to our guide for instructions on how to provision and configure a Keycloak server.

    import { createOidc } from "oidc-spa";
    

    This piece of code should give you the necessary information to understand how oidc-spa can be used inside your react components. To go further you can refer to the examples setup to see how to integrate oidc-spa with your routing library:

    If you get your OIDC parameters from an API you can passes an assync function that returns the oidc parameters. This function gets called when <OidcProvider /> is first mounted or when getOidc() is first called.

    xx

    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.

    import { createOidc } from "oidc-spa";
    

    Error Management

    What happens if the OIDC server is down, or if the server indicates that your client configuration is not valid?

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

    This allows the user to 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.

    Please note that due to browser security policies, it is impossible to distinguish whether the network is very slow or down, or if the OIDC server has rejected the configuration.

    Consequently, one might encounter an error of type "bad configuration" on a slow 3G network, for example.

    However, the timeout duration is automatically adjusted based on the speed of the internet connection of the user, which should prevent this issue from occurring.

    User impersonation

    Enable the admin of your application to login as a given user.

    circle-exclamation

    User impersonation should ideally be managed by the authentication server. For instance, if you are using Keycloak, you can navigate to the Admin Console, then go to: Users -> Action -> Impersonate. This allows you to access all applications within the realm as the impersonated user.

    The workaround described in this documentation is intended for situations where:

    End of third-party cookies

    circle-info

    TL;DR; It's mostly inconsequential.

    Google is ending third-party cookies for all Chrome users in 2024 and are already disabled by default in Safari.

    Let's see how it might affect you.

    First of all, if your identity server and your app shares the same root domain you are not affected.

    Example, if you are in the case:

    Doing Something Only When a New Session is Created

    In some cases, you might want to perform some operation to initialize the user's session. This could involve calling a special API endpoint or clearing some cached values in the local storage. What you don't want, however, is to run this every time the user refreshes the page or when their session is restored. To help you determine if the session should be initialized, you can leverage the authMethod property that is available when the user is logged in.

    There are three possible values for the authMethod property:

    1. "back from auth server": The user was redirected to the authentication server's login/registration page and then redirected back to the application. Assuming you are using Keycloak and if you have configured your Keycloak server as suggested in , this happens approximately once every 14 days, assuming the user is using the same browser and has not explicitly logged out. Of course, the 14-day session is just a good default if you don't want your user to go through the login process every day, but this is for you to decide. If you implement an

    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,
                publicUrl,
                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,
                publicUrl,
                decodedIdTokenSchema
            });
    https://github.com/keycloakify/oidc-spa/tree/main/examples/react-routergithub.comchevron-right
    Source code
    https://github.com/keycloakify/oidc-spa/blob/main/examples/tanstack-router/src/router/AutoLogoutCountdown.tsxgithub.comchevron-right
    Example implementation of a 60 seconds countdown before autologout.
    The demo app with a short SSO Session Idle
    https://github.com/keycloakify/oidc-spa/tree/main/examples/tanstack-router-file-basedgithub.comchevron-right
    Keycloak Configuration Guide
    import { createOidc } from "oidc-spa";
    
    const oidc = await createOidc({ ... });
    
    if( oidc.isUserLoggedIn ){
    
       // Function to invoke when the user click on your "change my password" button.
       const updatePassword = ()=>
          oidc.goToAuthServer({
             extraQueryParams: { 
                 kc_action: "UPDATE_PASSWORD" 
             }
          });
       // NOTE: This is optional, it enables you to display a feedback message
       // when the user is redirected back to your application after completing
       // or canceling the action.
       if( 
          oidc.authMethod === "back from auth server" &&
          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;
          }
       }
    }
    const oidc = await createOidc({
    issuerUri: "https://auth.your-domain.net/realms/myrealm",
    clientId: "myclient",
    /**
    * Vite: `publicUrl: import.meta.env.BASE_URL`
    * CRA: `publicUrl: process.env.PUBLIC_URL`
    * Other: `publicUrl: "/"` (Usually)
    */
    publicUrl: import.meta.env.BASE_URL
    });
    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,
    decodedIdToken
    } = oidc.getTokens();
    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" });
    // If you are wondering why ther's a decodedIdToken and no
    // decodedAccessToken read this: https://docs.oidc-spa.dev/resources/jwt-of-the-access-token
    console.log(`Hello ${decodedIdToken.preferred_username}`);
    // Note that in this example the decodedIdToken is not typed.
    // What is inside the idToken is defined by the OIDC server you are using.
    // If you want to specify the type of the decodedIdToken you can do:
    //
    // import { z } from "zod";
    // export const { useOidc } = createUseOidc({
    // ...
    // decodedIdTokenSchema: z.object({
    // sub: z.string(),
    // preferred_username: z.string(),
    // // ... other properties
    // })
    // })
    }
    react-router-dom example setup
    @tanstack/react-router example setup
    import { createOidc } from "oidc-spa";
    
    const oidc = await createOidc(...);
    
    if( !oidc.isUserLoggedIn ){
        // If the used is logged in we had no initialization error.
        return;
    }
    
    if( oidc.initializationError ){
    
        // initializationError.type can be either:
        // - "server down"
        // - "bad configuration"
        // - "unknown" 
        console.log(oidc.initializationError.type);
        
        const handleLoginClick = ()=> {
        
            if( oidc.initializationError ){
                alert(`Can't login now, try again later ${oidc.initializationError.message}`);
                return;
            }
            
            oidc.login(...);
        
        };
    }
    function ProtectedPage() {
        // Here we can safely assume that the user is logged in.
        const { goToAuthServer, backFromAuthServer } = useOidc({ assertUserLoggedIn: true });
    
        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>
                )}
            </>
        );
    }
    
    import { createReactOidc } from "oidc-spa/react";
    
    export const { OidcProvider, useOidc, getOidc } = createReactOidc({
        // NOTE: If you don't have the params right away see note below.
        issuerUri: "https://auth.your-domain.net/realms/myrealm",
        clientId: "myclient",
        /**
         * Vite:  `publicUrl: import.meta.env.BASE_URL`
         * CRA:   `publicUrl: process.env.PUBLIC_URL`
         * Other: `publicUrl: "/"` (Usually)
         */
        publicUrl: import.meta.env.BASE_URL
    });
    
    ReactDOM.createRoot(document.getElementById("root")!).render(
        <OidcProvider
            // Optional, it's usually so fast that a fallback is really not required.
            fallback={<>Checking authentication ⌛️</>}
        >
            <App />
        </OidcProvider>
    );
    
    function App() {
    
        const { isUserLoggedIn, login, logout, oidcTokens } = useOidc();
    
        return (
            isUserLoggedIn ? (
                <>
                    {/* 
                    Note: The decodedIdToken can be typed and validated with zod See: https://github.com/keycloakify/oidc-spa/blob/fddac99d2b49669a376f9a0b998a8954174d195e/examples/tanstack-router/src/oidc.tsx#L17-L43
                    If you are wondering why ther's a decodedIdToken and no
                    decodedAccessToken read this: https://docs.oidc-spa.dev/resources/jwt-of-the-access-token
                    */}
                    <span>Hello {oidcTokens.decodedIdToken.preferred_username}</span>
                    <button onClick={() => logout({ redirectTo: "home" })}>
                      Logout
                    </button>
                </>
            ) : (
                <button onClick={() => login({ 
                    /** 
                     * If you are calling login() in the callback of a button click
                     * (like here) 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 createReactOidc `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 user direcly to the register page
                     * See: https://github.com/keycloakify/oidc-spa/blob/14a3777601c50fa69d1221495d77668e97443119/examples/tanstack-router-file-based/src/components/Header.tsx#L54-L66
                     */
                })} >
                  Login
                </button>
            )
        );
    }
    
    import { useEffect, useState } from "react";
    
    type Order = {
      id: number;
      name: string;
    };
    
    function OrderHistory(){
    
        const { oidcTokens } = useOidc({ assertUserLoggedIn: true });
    
        const [orders, setOrders] = useState<Order[] | undefined>(undefined);
    
        useEffect(
            ()=> {
    
                fetch("https://api.your-domain.net/orders", {
                    headers: {
                        Authorization: `Bearer ${oidcTokens.accessToken}`
                    }
                })
                .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 const { 
      OidcProvide, 
      useOidc, 
      getOidc 
    } = createReactOidc(async ()=> {
    
        const { 
            issuerUri, 
            clientId 
        } = await axios.get("/oidc-params").then(r => r.data);
    
        return {
            issuerUri,
            clientId,
            publicUrl: import.meta.env.BASE_URL
        };
        
    });
    import { useOidc } from "oidc";
    import { useEffect } from "react";
    
    function LoginButton() {
    
        const { isUserLoggedIn, login, initializationError } = useOidc();
    
        useEffect(() => {
            if (!initializationError) {
                return;
            }
    
            console.warn("OIDC initialization error");
            switch (initializationError.type) {
                case "bad configuration":
                    console.warn("The identity server and/or the client is misconfigured");
                    break;
                case "server down":
                    console.warn("The identity server is down");
                    break;
                case "unknown":
                    console.warn("An unknown error occurred");
                    break;
            }
        }, []);
    
        if (isUserLoggedIn) {
            return null;
        }
    
        return (
            <button onClick={() => {
    
                if (initializationError) {
                    alert(`Can't login now, try again later: ${
                        initializationError.message
                    }`);
                    return;
                }
    
                login({ ... });
    
            }}>
                Login
            </button>
        );
    
    }
    import { createMockOidc } from "oidc-spa/mock";
    import { z } from "zod";
    const decodedIdTokenSchema = z.object({
    sub: z.string(),
    preferred_username: z.string()
    });
    const publicUrl= import.meta.env.BASE_URL;
    const oidc = !import.meta.env.VITE_OIDC_ISSUER
    ? await createMockOidc({
    isUserInitiallyLoggedIn: false,
    publicUrl,
    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,
    publicUrl
    });
  • The support team handling impersonation does not have access to the Keycloak Admin Console.

  • Hosting a custom admin app under a subpath of the authentication server's URL is not an option.

  • Imagine you have a custom admin app that allows your support team to impersonate users. With oidc-spa, you can include a special query parameter when redirecting a support team member from your admin app to your main app. This will automatically authenticates the support team member as the impersonated user.

    By default, this feature is disabled. To enable it:

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

    hashtag
    Crafting the URL for Impersonation

    After using the Keycloak API to obtain an access token, ID token, and refresh token for a user session in exchange for your admin token, you can craft the redirection URL for impersonation as follows:

    (For this example, we assume you're using a JavaScript backend, but you can easily adapt it to your environment.)

    Your app is hosted at www.example.com or dashboard.example.com

  • Your identity server, for example Keycloak, is hosted at: auth.example.com

  • You are not affected ✅. Indeed Both www.example.com, dashboard.example.com and auth.example.com shares the same root domain: example.com. On the other end, if you are in the folowing case:

    • You app is hosted at www.examples.com or dashboard.example.com

    • Your identity server is hosted at: auth.sowhere-else.com

    Let's see how third party cookies phase out will affect you:

    • You will see a console warning "Third-party cookie will be blocked" in the console in production.

    • If a user that is authenticated close the tab of your app or close the browser and open your site again a while later. With third party cookies enabled and assuming he's session haven't expired yet he will be automaticall logged in. With third party cookies disabled your website will load in unautenticated mode. If he clicks on the login button this will trigger a full reload and he will be authenticated without having to enter he's credential again.

    Conex resources:

    hashtag
    Google reCaptcha

    reCaptcha is not directly related to oidc-spa since the cookie it sets is on the thegister page (so outside of your app). Anyway, since it's a connex concern:

    , it will be, of course, much shorter.
  • "session storage": The user's authentication was restored from the browser session storage, typically after a page refresh. As soon as the user closes the tab, the session storage is cleared. Note that you can disable the persitance of the token in the session storage. Note as well that if you use user inpersonation this is the status you'll get.

  • "silent signin": The user was authenticated silently using an iframe to check the session with the authentication server. This happens most of the time when the user navigates to your app in a new tab and their session has not expired yet.

  • import { createOidc } from "oidc-spa";
    
    src/oidc.ts
    import { createReactOidc } from
    

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

    import { useOidc } from "./oidc";
    

    the configuration guide
    Auto Logout mechanism

    Sponsors

    Backers of the project

    Join the Keycloakify Discord Server!Discordchevron-right

    Keycloak Configuration Guide

    Let's spin up a Keycloak server and configure it for your webapp!

    hashtag
    Provisioning a Keycloak server

    If you already have access to a Keycloak server you can skip this section.

    Follow one of the following guides:

    
    const accessToken = "...";
    const idToken = "...";
    const refreshToken = "...";
    
    const obj = {
        accessToken,
        idToken,
        refreshToken,
    };
    
    // NOTE: An array in case you have more than one oidc client instance in your app.
    const arr = [obj];
    const str = JSON.stringify(arr);
    const b64 = btoa(str); // to base64
    
    // This is the impersonation url:
    const url = `https://your-app.com?oidc-spa_impersonate=${b64}`
    const oidc = await createOidc({
    // ...
    getDoContinueWithImpersonation: async ({ parsedAccessToken })=> {
    const doContinue = confirm(`
    WARNING: You are about to impersonate ${parsedAccessToken.email}.
    If you don't understand why you are seeing this message please
    click cancel and contact support.
    Someone might be trying to trick you.
    `);
    return doContinue;
    }
    });
    export const { OidcProvider, useOidc, getOidc } = createReactOidc({
    // ...
    getDoContinueWithImpersonation: async ({ parsedAccessToken })=> {
    const doContinue = confirm(`
    WARNING: You are about to impersonate ${parsedAccessToken.email}.
    If you don't understand why you are seeing this message please
    click cancel and contact support.
    Someone might be trying to trick you.
    `);
    return doContinue;
    }
    });
    const oidc = await createOidc({ /* ... */ });
    if (oidc.isUserLoggedIn && oidc.isAuthMethod === "back from auth server") {
    // Do something related to initialization or clearing cache
    }
    "
    oidc-spa/react
    "
    ;
    export const {
    /* ... */
    getOidc
    } = createReactOidc({ /* ... */ });
    getOidc().then(oidc => {
    if( !oidc.isUserLoggedIn ){
    return;
    }
    if( oidc.authMethod === "back from auth server" ){
    // Do something related to initialization or clearing cache
    }
    });
    import { useEffect } from "react";
    function MyComponent(){
    const { isUserLoggedIn, authMethod } = useOidc();
    useEffect(()=> {
    // Warning! In dev mode, when React Strict Mode is enabled
    // this will be called twice!
    if( !isUserLoggedIn ){
    return;
    }
    if( authMethod === "back from auth server" ){
    // Do something related to initialization or clearing cache
    }
    }, []);
    }
    Logo
    Don't want to deploy and maintain a own Keycloak server yourself?

    Choosing Keycloak as a Service through a cloud IAM provider can offload the complexities of management and maintenance. It ensures that your system is always up-to-date with the latest security patches and features without the direct overhead of server upkeep. This is especially beneficial for teams prioritizing development and innovation over infrastructure management, offering robust support and service level agreements to guarantee smooth operation.

    hashtag
    Configuring your Keycloak server

    Let's configure your Keycloak server with good default for an SPA.

    Connect to the admin panel of your Keycloak server (we assumes it's https://auth.my-domain.net/auth)

    • Create a realm called "myrealm" (or something else), go to Realm settings

      1. On the tab General

        1. User Profile Enabled: On

      2. On the tab login

        1. User registration: On

        2. Forgot password: On

      3. On the tab email, we give an example with , if you don't have a SMTP server at hand you can skip this by going to Authentication (on the left panel) -> Tab Required Actions -> Uncheck "set as default action" Verify Email. Be aware that with email verification disable, anyone will be able to sign up to your service.

        1. From: [email protected]

      4. On the tab Themes. See for creating a Keycloak theme that match your webapp.

      5. On the tab Localization

        1. Internationalization: Enabled

        2. Supported locales: <Select the languages you wish to support>

      6. On the tab Sessions

        1. SSO Session Idle: 14 days - This is where you configure the policy. This parameter defines the lifespawn of the refresh token. If you want your user to be automatically loged out after 30 minutes of inactivity, Inacivity meaning they are not actively interacting with your app by scrolling, moving the mouse or typing, then you want to set this to 30 minutes.

        2. SSO Session Max:

    • Create a new OpenID Connect client called "myclient" (or something else) by accessing Clients -> Create Client

      1. Root URL: https://your-domain.net (or something else, your app does not need to be on the

      the same domain as your Keycloak).

    • (OPTIONAL) In Authentication (on the left panel) -> Tab Required Actions enable and set as default action Terms and Conditions. (You can use Keycloakify to specify your terms and conditions, see next section)

    • (OPTIONAL) On the left pannel you can go to identity provider to enable login via Google, GitHub, Instagram, ect...

    • (OPTIONAL) Enable your user to delete their own account (see )

      1. In the left bar navigate to Autentication -> Required Action -> "Delete Account" Enabled: On

      2. In the left bar navigate to Realm Setting -> User Registration -> Default Roles -> Assign Role ->

    circle-check

    Now the parameter that you will have to provide to oidc-spa are:

    Replace your-domain.net, myrealm and myclient by what you actually used in the configuration process. (On older Keycloak the issuerUri will be "https://auth.your-domain.net/auth/realms/myrealm")

    oidc-spa/examples/tanstack-router/src/oidc.tsx at a5dbcca428721c4ea4ed1fd36dc07e80fc9d2cb0 · keycloakify/oidc-spaGitHubchevron-right
        issuerUri: "https://auth.your-domain.net/realms/myrealm",
        clientId: "myclient"
    Remember me: On, or Off if you want to implement .
    Host: email-smtp.us-east-2.amazonaws.com
  • Port: 465

  • Authentication: enabled

  • Username: **************

  • Password: ***************************************

  • When clicking "save" you'll be asked for a test email, you have to provide one that correspond to a pre-existing user or you will get a silent error and the credentials won't be saved.

  • 14 days
    - Even if you implement
    you want to leave this to at least one day. Indeed as long as your users are actively interacting with your app they should remain logged in**.**
  • SSO Session Idle Remember Me: 365 days - Same than SSO Session Idle but when the user have checked "Remember me" when login in. If you have enaled "remeber me" and you want this option to make sens you must set it to a value that is greater than SSO Session Idle. If you have set SSO Session Idle to something short because you want to implement an auto logout policy you probably want to go in Realm -> login and disable "Remember me"

  • SSO Session Max Remember Me: 365 days - Same note here.

  • Valid redirect URIs: https://onyxia.my-domain.net/*, http://localhost* (for testing in local)
  • Web origins: *

  • Login theme: keycloak (or your theme if you have one)

  • Filter by client
    -> select
    Delete Account
    and click on assign.
    AWS SESarrow-up-right
    Keycloakifyarrow-up-right
    auto logout
    user account managment
    auto logout
    auto logout
    Keycloak Consulting - Enterprise IAM That Actually WorksZone2chevron-right
    Keycloak Consulting Services - Your partner in Keycloak deployment, configuration, and extension development .
    Cloud identity and acces managementcloud-iam.comchevron-right
    Keycloak as a Service. Use code 'keycloakify5' at checkout for a 5% discount.
    https://docs.keycloakify.dev/faq-and-help/google-recaptcha-and-end-of-third-party-cookiesdocs.keycloakify.devchevron-right
    Logo
    Managed Keycloak Hosting and Enterprise Keycloak Supportphasetwo.iochevron-right
    Keycloak community contributors of popular extensionsarrow-up-right providing free and dedicated Keycloak hostingarrow-up-right and enterprise Keycloak supportarrow-up-right to businesses of all sizes.
    Logo

    Web API

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

    Let's see a very basic REST API example:

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

    src/oidc.ts
    import { createOidc } from
    

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

    src/api.ts
    import axios from "axios"
    

    Initialize the React adapter of oidc-spa and expose the prOidc object, a promise of the vanilla OIDC API:

    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:

    circle-info

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

    hashtag
    Backend

    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:

    hashtag
    Comprehensive example

    If you're looking for a comprehensive Backend+Frontend example you can refer to Insee's project

    The app is live here:

    The frontend (Vite project):

    The backend (Node TODO App REST API):

    src/oidc.ts
    import { createReactOidc } from "oidc-spa/react";
    
    export const { 
        OidcProvider, 
        useOidc,
        getOidc
    } = createReactOidc(/* ... */);
    "
    oidc-spa
    "
    ;
    export const prOidc = createOidc({/* ... */});
    ;
    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");
    }
    config.headers.Authorization = `Bearer ${oidc.getTokens().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)
    };
    tRPCarrow-up-right
    TanStack Queryarrow-up-right
    Write a blog post about 3rd party cookie deprecation · Issue #25990 · keycloak/keycloakGitHubchevron-right
    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");
        }
        
        config.headers.Authorization = `Bearer ${oidc.getTokens().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";
    
    export async function createDecodeAccessToken() {
    
        const oidcIssuerUri = process.env.OIDC_ISSUER
    
        if (oidcIssuerUri === undefined) {
            throw new Error("OIDC_ISSUER must be defined in the environment variables")
        }
    
        const { verifyAndDecodeAccessToken } = await createOidcBackend({ 
            issuerUri: oidcIssuerUri,
            decodedAccessTokenSchema: z.object({
                sub: 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()
            })
        });
    
        function decodeAccessToken(params: { 
            authorizationHeaderValue: string | undefined; 
            requiredRole?: string;
        }) {
    
            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.ream_access.roles.includes(requiredRole)
            ){
                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();
    
        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")
                });
    
                if (decodedAccessToken === undefined) {
                    throw new HTTPException(401);
                }
    
                const todos = 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`)
    
    })();
    Logo
    Logo
    OpenShiftKeycloakchevron-right
    Get started with Keycloak on OpenShift
    KubernetesKeycloakchevron-right
    Get started with Keycloak on Kubernetes
    Cloud identity and acces managementwww.cloud-iam.comchevron-right
    DockerKeycloakchevron-right
    Get started with Keycloak on Docker
    PodmanKeycloakchevron-right
    Get started with Keycloak on Podman
    OpenJDKKeycloakchevron-right
    Get started with Keycloak on bare metal
    Logo
    Logo
    Logo
    Logo
    Logo
    Logo
    Logo
    3rd party cookies phase out on chrome · Issue #26128 · keycloak/keycloakGitHubchevron-right
    Insee SPA Startervite-insee-starter.demo-domain.ovhchevron-right
    GitHub - InseeFrLab/vite-insee-starter: A template project for SPAsGitHubchevron-right
    GitHub - InseeFrLab/todo-rest-api: Todo app rest API with OIDC and OpenAPIGitHubchevron-right
    Logo
    Logo
    Logo
    Logo