Thanks to visit codestin.com
Credit goes to github.com

Skip to content

keycloakify/oidc-spa

Repository files navigation

oidc-spa


We're here to help!

Home - Documentation

At a glance

The Framework Agnostic Adapter:

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

const oidc = await createOidc({
    issuerUri: "https://auth.my-domain.net/realms/myrealm",
    //issuerUri: "https://login.microsoftonline.com/...",
    //issuerUri: "https://xxx.us.auth0.com/..."
    //issuerUri: "https://accounts.google.com/o/oauth2/v2/auth"
    clientId: "myclient",
    // Optional, for type safety.
    decodedIdTokenSchema: z.object({
        name: z.string(),
        picture: z.string().optional(),
        email: z.string(),
        realm_access: z.object({ roles: z.array(z.string()) })
    })
    // Yes really, it's that simple no other params to provide.
    // The Redirect URI (callback url) is the root url of your app.
});

if (!oidc.isUserLoggedIn) {
    oidc.login();
    return;
}

const { name, realm_access } = oidc.getDecodedIdToken();

console.log(`Hello ${name}`);

const { accessToken } = await oidc.getTokens();

await fetch("https://my-domain.net/api/todos", {
    headers: {
        Authorization: `Bearer ${accessToken}`
    }
});

if (realm_access.roles.includes("realm-admin")) {
    // User is an admin
}

Higher level adapters, example with React but we also feature similar Angular adapter:

Image

Full Stack Auth solution with TanStack Start:

import { createServerFn } from "@tanstack/react-start";
import { enforceLogin, oidcFnMiddleware } from "@/oidc";
import fs from "node:fs/promises";

const getTodos = createServerFn({ method: "GET" })
    .middleware([oidcFnMiddleware({ assert: "user logged in" })])
    .handler(async ({ context: { oidc } }) => {
        const userId = oidc.accessTokenClaims.sub;

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

        return JSON.parse(json);
    });

export const Route = createFileRoute("/todos")({
    beforeLoad: enforceLogin,
    loader: () => getTodos(),
    component: RouteComponent
});

function RouteComponent() {
    const todos = Route.useLoaderData();

    return (
        <ul>
            {todos.map(todo => (
                <li key={todo.id}>
                    {todo.isDone && "✅"} {todo.text}
                </li>
            ))}
        </ul>
    );
}

What this is

oidc-spa is a framework-agnostic OpenID Connect client for browser-centric web applications implementing the Authorization Code Flow with PKCE.

It work with any spec compliant OIDC provider like Keycloak, Auth0 or Microsoft EntraID and replace provider-specific SDKs like keycloak-js, auth0-spa-js, or @azure/msal-browser with one unified API, freeing your app from vendor lock-in and making it deployable in any IT system.
Concretely this mean that it let you build an app and sell it to different companies ensuring they will be able to deploy it in their environment regardless of what auth platform they use internally.

oidc-spa provides strong guarantees regarding the protection of your tokens even in case of successful XSS or supply chain attacks. No other implementation can currently claim that.

It is uncompromising in terms of performance, security, DX, and UX. You get a state-of-the-art authentication and authorization system out of the box with zero glue code to write and no knobs to adjust.

Unlike server-centric solutions such as Auth.js, oidc-spa makes the frontend the OIDC client in your IdP model's representation.

Your backend becomes a simple OAuth2 resource server that you frontend query with the access token attached as Authorization header. oidc-spa also provides the tools for token validation on the server side:

That means no database, no session store, and enterprise-grade UX out of the box, while scaling naturally to edge runtimes.

oidc-spa exposes real OIDC primitives, decoded ID tokens, access tokens, and claims, instead of hiding them behind a “user” object, helping you understand and control your security posture.

It’s infra-light, open-standard, transparent, and ready to work in minutes.

But in details? I want to understand the tradeoffs.

In the modern tech ecosystem, no one “rolls their own auth” anymore, not even OpenAI or Vercel.
Authentication has become a platform concern. Whether you host your own identity provider like Keycloak, or use a service such as Auth0 or Microsoft Entra ID, authentication today means redirecting users to your auth provider.


Why not BetterAuth or Auth.js

These are great for what they are, but they’re “roll your own auth” solutions.
With oidc-spa, you delegate authentication to a specialized identity provider such as Keycloak, Auth0, Okta, or Clerk.

With BetterAuth, your backend is the authorization server, even if you can integrate third party identity providers id doesn't change that fact.
That’s very battery-included, but also far heavier infrastructure-wise.
Today, very few companies still roll their own auth, not even OpenAI or Vercel.

Another big difference: oidc-spa is browser-centric. The token exchange happens on the client,
and the backend server is merely an OAuth2 resource server in the OIDC model.

If you use BetterAuth to provide login via Keycloak, your backend becomes the OIDC client application,
which has some security benefits over browser token exchange, but at the cost of centralization and requiring backend infrastructure.

One clear advantage BetterAuth has over oidc-spa is more natural SSR support. In the oidc-spa model, the server doesn’t know the authentication state of the user at all time, which makes it difficult to integrate with traditional full-stack frameworks that rely on server-side rendering.


Server Side Rendering

The only SSR-capable framework we currently support is TanStack Start, because it provides the low-level primitives needed to render as much as possible on the server while deferring rendering of auth aware components to the client.

This approach achieves a similar UX and performance to server-centric frameworks, but it’s inherently less transparent than streaming fully authenticated components to the client.

Try the TansStack Start example deployment with JavaScript disabled to get a feel of what can and can't be SSR'd: https://example-tanstack-start.oidc-spa.dev/


Security and XSS resilience

Yes; client-side authentication raises valid security concerns.
But this isn’t a fatal flaw; it’s an engineering challenge, and oidc-spa addresses it head-on.

It treats the browser as a hostile environment, going to great lengths to protect tokens even under XSS or supply-chain attacks.
These mitigations are documented here.


Limitations regarding backend delegation

The main limitation is with long-running background operations.
If your backend must call third-party APIs on behalf of the user while they’re offline, you’ll need service accounts for those APIs or take charge of rotating tokens yourself which can be tricky.
Beyond that, everything else scalability, DX, performance, works in your favor.


If that all sounds good to you…
Let’s get started.

Integration

Comparison with Existing Libraries

With other OIDC clients, you'll get something that works in the happy path.
But then you will face issues like:

  • The user cannot navigate back from the login page to your app.
  • User get hit with "your session has expired please login again" after spending an hours filling your form.
  • User logged out or logged in on one tabe but the state is not propagated to the other tabs.
  • Random 401 from your API with "token expired"
  • You can't run E2E test without having to actually connect to a real server.

Plus you'll realize that your configuration works with one provider in one devloppement configuration, try to switch IdP and the all thing fall appart, you'll be met with criptic errors and have to spend days tweeking knobs again.

With oidc-spa, there's no knobs to adjust, things just work out of the box.
And you get XSS and suply chain attack protection, unlike with any other client side solution.

It is a great low-level implementation of the OIDC primitives.
But it is only that, a low-level implementation. If you want to use it in your application, you’ll have to write a ton of glue code to achieve a state-of-the-art UX,
code that has no business living in application-level logic.

Example of what you get out of the box with oidc-spa:

  • Login/logout propagation across tabs
  • SSO that just works, regardless of the deployment configuration.
  • Seamless browser back/forward cache (bfcache) management
  • Auto logout: avoid "your session has expired please login again" after the used just spend an hour filling your form.
  • Never getting an expired access token error, even after waking from sleep
  • Graceful handling when the provider lacks refresh tokens or a logout endpoint (e.g. Google OAuth)
  • Mock support, run with a mock identity without contacting a server
  • Helpfull debug logs important if you sell your solution, to streamline deployment.

oidc-spa just works. You provide the few parameters required to talk to your IdP, and that’s it.

On top of that, oidc-spa provides much stronger security guarantees than oidc-client-ts does out of the box,
and yields a level of performance that isn’t realistically achievable with the tools oidc-client-ts alone provides.

react-oidc-context is a thin React wrapper around oidc-client-ts.
oidc-client-ts compares to oidc-spa/core,
and react-oidc-context compares to oidc-spa/react-spa.

oidc-spa provides much better security, DX, and UX out of the box.

NOTE: You can use oidc-spa/keycloak-js as a literal drop-in replacement for keycloak-js
your app will instantly perform better, be much more secure, and implement session expiration correctly.

The official OIDC client for Keycloak has several issues:

  • Does not respect the OIDC spec, hence only works with Keycloak, requiring a wildcard in valid redirect URIs, which is problematic.
  • Its API encourages incorrect usage, e.g., by directly exposing the access token via a synchronous API..
  • Does not expose high-level adapters for React or Angular, requiring you to write your own wrappers.
  • Does not handle redirects correctly: once on the login page, you can’t go back.
  • Makes no attempt to protect tokens against XSS attacks.
  • You can't talk to more than one resource server.
  • Does not handle session expiration; users aren’t automatically logged out or warned before expiration.
  • Lacks mock implementations for testing against mock identities.

oidc-spa exports oidc-spa/keycloak providing all the keycloak specific feature that keycloak-js offers.

oidc-spa even comes with a polyfill implementation of the keycloak-js API.

It’s an Angular wrapper for keycloak-js, with the same limitations as above.
oidc-spa exposes an Angular adapter: oidc-spa/angular.

This is a solid generic OIDC adapter.
However, oidc-spa/angular still has several advantages:

  • Better security guarantees (angular-oauth2-oidc does not protect tokens from XSS or supply-chain attacks)
  • DX more aligned with modern Angular.
  • Auto logout overlay (“Are you still there?” countdown)
  • Stronger type safety with propagated user profile types
  • Ability to start rendering before session restoration settles
  • Support for multiple resource servers
  • Clearer and more actionable error messages for misconfiguration

These are great for what they are, but they’re “roll your own auth” solutions.
With oidc-spa, you delegate authentication to a specialized identity provider such as Keycloak, Auth0, Okta, or Clerk.

With BetterAuth, your backend is the authorization server. (even if you can integrate third party provider).
That’s very battery-included, but also far heavier infrastructure-wise.
Today, very few companies still roll their own auth—including OpenAI and Vercel.

Another big difference: oidc-spa is browser-centric. The token exchange happens on the client,
and the backend server is merely an OAuth2 resource server in the OIDC model.

If you use BetterAuth to provide login via Keycloak, your backend becomes the OIDC client application,
which has some security benefits over browser token exchange, but at the cost of centralization and requiring backend infrastructure.

And if we look closer, it can feel reassuring, in the Auth.js or BetterAuth model, to know that tokens are never directly exposed to JavaScript.
However, it’s important to understand that if an attacker successfully injects and executes code within your app’s origin, there are effectively no restrictions on the requests they can make.
Even without having access to the token itself, the attacker can perform any action on behalf of the user, since authentication is automatically handled via cookies.

With oidc-spa, the situation is different. Even in the event of an XSS attack, the attacker cannot send authenticated requests to the server unless they manually attach an access token.
Those tokens do exist in memory, but they are unreachable, and impossible to request, the runtime environment is hardened before any JS code other than oidc-spa gets a chance to run, making token extraction virtually impossible and ultimately making the attack harmless.

One clear advantage BetterAuth has over oidc-spa is better SSR (Server-Side Rendering) support. In the oidc-spa model, authentication is handled entirely on the client, which makes it challenging to integrate with traditional full-stack frameworks that depend on server-side rendering.

Currently, the only SSR-capable framework we support is TanStack Start, which provides the low-level primitives required to render as much as possible on the server while deferring user-specific components to the client. We won’t pretend this is a small limitation, it significantly restricts what can actually be SSR’d. In practice, you can only server render content that’s identical for every user (such as the marketing pages and layout), while everything user-dependent must be rendered client-side.

This doesn’t hurt UX or performance, but it’s inherently less flexible than streaming fully authenticated server components to the client. That said, with TanStack Start, the abstractions are elegant enough that this separation is mostly transparent, it just works.

When it comes to Next.js or React Router frameworks with server features, however, we currently can’t offer a convincing solution.

The strength of oidc-spa lies elsewhere: it’s extremely lightweight, requiring no backend, no extra infrastructure, and scaling effortlessly at the edge. It’s fast, secure, and keeps your deployments simple. The trade-off is that SSR becomes more difficult, though, as shown in the TanStack Start example, not impossible.

Acknowledgment

oidc-spa vendors oidc-client-ts for its frontend logic and jose for token validation on the backend.
The idea was to build on top of battle-tested primitives.
We appreciate what we owe to those projects.

🚀 Quick start

Head over to the documentation website 📘!

Sponsors

Project backers — we trust and recommend their services.


Logo Dark

Logo Light


Keycloak as a Service — Keycloak community contributors of popular extensions providing free and dedicated Keycloak hosting and enterprise Keycloak support to businesses of all sizes.




Logo Dark

Logo Light


Keycloak Consulting Services — Your partner in Keycloak deployment, configuration, and extension development for optimized identity management solutions.

About

Openid connect client for Single Page Applications

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Contributors 4

  •  
  •  
  •  
  •