Framework Agnostic Adapter

Let's get your App authenticated!

oidc-spa is framework-agnostic, but it’s also client-centric.

It integrates seamlessly with any Single Page Application, but not with full-stack frameworks that rely on server-side rendering (like Next.js).

The only full-stack framework currently supported is TanStack Start, which aligns with oidc-spa’s client-first, server capable architecture.

Note that if you’re using React but not TanStack or React Router, you’ll likely still benefit more from oidc-spa/react-spa than from oidc-spa/core.

In that case, follow the React Router integration guide in Declarative Mode, it should be easy to adapt to your setup.

Installation

npm install oidc-spa

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

This design provides much stronger security guarantees than any other adapter, and it also delivers unmatched login performance. More details here.

In Vite apps, this is done through a Vite Plugin (If you'd rather avoid using the Vite plugin checkout the Other SPAs tab).

vite.config.ts
import { defineConfig } from "vite";
import { oidcSpa } from "oidc-spa/vite-plugin";

export default defineConfig({
    plugins: [
        // ...
        oidcSpa({
            freezeFetch: true,
            freezeXMLHttpRequest: true,
            freezeWebSocket: true
        })
    ]
});

Basic Usage

import { createOidc } from "oidc-spa/core";
import { z } from "zod";

const oidc = await createOidc({
    issuerUri: "https://auth.your-domain.net/realms/myrealm",
    clientId: "myclient",
    //scopes: ["profile", "email", "api://my-app/access_as_user"],
    extraQueryParams: () => ({
       ui_locales: "en" // Keycloak login/register pages language
       //audience: "https://my-app.my-company.com/api"
    }),
    // This is declarative, you declare what you will use and what
    // infos you expect to be present in the id token.
    // if you don't know what's in your id token open the console
    // if you have debugLogs set to true you'll see.
    decodedIdTokenSchema: z.object({
       preferred_username: z.string(),
       name: z.string()
       email: z.string().email().optional(),
       picture: z.string().optional(),
       email: z.string().email().optional(),
       realm_access: z.object({ roles: z.array(z.string()) }).optional()
    }),
    debugLogs: true
});

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.  
          * If you are calling this because the user has navigated to
          * a route that requires them to be logged in, set this to true.
          */
         doesCurrentHrefRequiresAuth: false
         /** 
          * Optionally, you can add 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 but by default it's the current url
          * which is usually what you want.
          */
          // redirectUrl: "/dashboard"
    });
    
    // oidc-spa export keycloak specific tooling:
    const { isKeycloak, createKeycloakUtils } = await import("oidc-spa/keycloak");
    
    // If your IdP is a Keycloak
    if( isKeycloak({ issuerUri: oidc.issuerUri }) ){
        const keycloakUtils = createKeycloakUtils({ issuerUri: oidc.params.issuerUri });
        // Redirect directly to the register page instead of the login page
        oidc.login({
            doesCurrentHrefRequiresAuth: false,
            transformUrlBeforeRedirect: keycloakUtils.transformUrlBeforeRedirectForRegister
        });
        
    }
    

} 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();
    
    // oidc-spa also provide the toold to create such an API.
    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}`);
    
    // Get a link to the account page:
    const userAccountUrl = keycloakUtils.getAccountUrl({ 
        clientId: oidc.params.issuerUri,
        validRedirectUri: oidc.params.validRedirectUri
    });
    
}

Mock Adapter

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

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

import { createOidc } from "oidc-spa/core";
import { createMockOidc } from "oidc-spa/mock";
import { z } from "zod";

const decodedIdTokenSchema = z.object({
    sub: z.string(),
    preferred_username: z.string()
});

const autoLogin = false;

const oidc = !import.meta.env.VITE_OIDC_ISSUER
    ? await createMockOidc({
          // NOTE: If autoLogin is set to true this option must be removed
          isUserInitiallyLoggedIn: false,
          mockedTokens: {
              decodedIdToken: {
                  sub: "123",
                  preferred_username: "john doe"
              } satisfies z.infer<typeof decodedIdTokenSchema>
          },
          autoLogin
      })
    : await createOidc({
          issuerUri: import.meta.env.VITE_OIDC_ISSUER,
          clientId: import.meta.env.VITE_OIDC_CLIENT_ID,
          decodedIdTokenSchema,
          autoLogin
      });

Creating an API server

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

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

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

You’ll find the full documentation here:

Creating an API Server

Last updated

Was this helpful?