Repository navigation
Replies: 1 comment 4 replies
|
I’m considering naming this utility simply , since it behaves identically for everything else, meaning you could use it everywhere you’d normally use a Suspense boundary, and it would just work. I would not use the Same Name for This. Ide autocomplete will mess this up and you will get the wrong Import. In auth case i think it's a Security concern to have the wrong I think. |
4 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Context
oidc-spais a client-side implementation of the OpenID Connect Authorization Code Flow + PKCE.All token exchanges with the authorization server happen entirely in the browser.
This approach offers major benefits:
We discuss why we deliberately avoid involving the backend in the token exchange process in this article:
“Why No Client Secret?”.
However, this approach introduces two core challenges:
oidc-spamitigates this through strict isolation of tokens and novel protection techniquesagainst XSS and supply chain compromise.
(See my Keycloak annual conference talk.)
knowing the user’s authentication state. This makes SSR and server data loading trickier.
This RFC describes how to solve both challenges cleanly in TanStack Start,
while keeping the developer experience (DX) consistent with idiomatic React/TanStack patterns.
The Challenge: SSR & Server Functions
Because tokens are exchanged on the client, the server doesn’t automatically know who the user is.
This makes it difficult to:
todosfrom a DB).This typically affects:
We need a way to handle these cases without rewriting mental models or breaking SSR entirely.
Why Other SDKs Don’t Have This Problem
Libraries like Clerk or NextAuth manage authentication server-side.
The server owns token exchange, so it always knows the auth state.
But this comes at a cost, deeper coupling between the framwork and the auth requires a database to store session, or vendor lock in to a specic auth provider.
Our goal: Keep the simplicity of client-only OIDC, while retaining full-stack capabilities.
Solving SSR
There are three main reasons for using SSR:
Since the auth state only exists client-side, full SSR isn’t possible for pages that render content contingent on the authentication state or user identity.
But partial SSR is, we can still server-render everything except the parts that depend on authentication.
Example
Using suspense to defer the rendering of the auth button to the client.
This is what it'll look like in practice (the video is slowed down, it goes much faster):
Screen_Recording_2025-09-30_at_13.44.21.mov
💡 Key Insight:
Use React Suspense to defer the rendering of components using
useOidcto the clienteverything else can be SSR’d.
Now, how do we provide an experience that avoids overfetching in TanStack Start?
TanStack Start doesn’t yet support Server Components, so we can set those aside for now.
What it does have are loaders, functions that can run isomorphically on both the server and client, allowing you to prefetch the data required to render a route.
Here’s a typical example:
src/api.todos.tsIn this configuration, the
/api/todosendpoint requires an access token.The issue arises when the loader runs on the server, since the server doesn’t know the authentication state, meaning we can’t use
getOidcAccessToken()there.The only option is to disable SSR (ssr: false) for this route so it runs entirely on the client:
This works, but it doesn’t solve the second issue: overfetching.
It’s fine if the page isn’t fully SSR’d (we still benefit from SSR for the static layout and metadata, which is the more important).
The real concern is that the client might fetch far more data than needed.
For instance, if the
/api/todosendpoint returns detailed metadata about each todo, but the page only renderstextandisDone, we’re transferring unnecessary data.That’s one of the main reasons SPA architectures have a reputation for being inefficient: they often overfetch.
To achieve optimal performance without forcing overly fine-grained APIs, we need a way to ensure that only the required data is sent to the client.
The solution: Server Functions.
In this model, the
/api/todosendpoint is requested on the server and only the data actually needed for rendering is sent to the client.This works because
ssr: falseensures the loader is executed exclusively on the client, while thegetTodosfunction itself runs on the server.Since it’s client-initiated, we can safely forward the authentication context, including the
accessToken.To sum up, the rules for using
oidc-spain TanStack Start are:useOidcwithin a narrowly scoped Suspense boundary.ssr: false), oruseEffect()callback (which always runs client-side).That’s it.
These are very reasonable tradeoffs for what you gain: a full-stack, provider-agnostic authentication solution.
Learning
oidc-spaisn’t just learning a bespoke SDK, it’s learning core concept of OpenID Connect standard through a portable solution.Now that we’ve covered the theoretical foundation, let’s move on to the actual API that
oidc-spaexposes.Example usage
src/oidc.tsAbout
OidcSuspenseAs mentioned earlier when discussing SSR boundaries, I initially hoped to let users rely solely on React’s built-in
<Suspense />.Unfortunately, there’s currently no way (while rendering on the server) to signal “this can’t be rendered here, defer to the nearest Suspense boundary and let the client continue.”
Throwing a never-resolving suspense works, but it leaves the server waiting for a stream that never completes — causing the loading tab to spin indefinitely.
Until React offers more granular control over suspense semantics, you need to use the provided wrapper:
I’m considering naming this utility simply
<Suspense />, since it behaves identically for everything else, meaning you could use it everywhere you’d normally use a Suspense boundary, and it would just work.About
getOidcFnMiddlewareThe
hasRequiredClaimsoption is mandatory, without it, it’s too easy for developers to write code like this:In this example, the user checks permissions on the client before making an RPC call, but their server function might neglects to enforce them server-side.
That’s dangerous, the server must always verify claims again.
Hence,
hasRequiredClaimsensures the boundary is enforced properly at the costof making user write
hasRequiredClaims: ()=> truea lot.About
getOidcRequestMiddlewareWorks the same way as
getOidcFnMiddleware, but for API endpoints instead of server functions.Talking to multiple OAuth2-enabled resource servers
In most cases, when we talk about a “web app,” we’re referring to the pair (client, dedicated API).
This boundary becomes less clear with full-stack frameworks like Next.js or TanStack Start, but the conceptual model remains valid.
By default, when you call
.withAccessTokenValidation(),oidc-spaassumes that the resource server for which the access token is valid is everything that runs on thebackend in your TanStack Start app: Server Function and API handlers.
However, this isn’t always the case, your app might also need to talk directly to other resource servers, such as an S3-compatible storage (MinIO), a Vault server, or the Kubernetes API.
Often, your backend acts as a proxy to these services.
For example, when interacting with Kubernetes, you might call a backend endpoint that forwards requests and injects credentials.
But this isn’t required, your app can just as well talk directly to multiple resource servers, especially in SPA or Electron-like environments where the frontend is the primary application and the backend remains stateless.
RFC 8707, Resource Indicators for OAuth 2.0
The OIDC specification includes RFC 8707, which allows a client to request access tokens for a specific resource server.
Providers like Auth0 or Entra ID support this pattern:
you register a client and separately register APIs (resource servers), then define which APIs each client can access.
oidc-spa’s core supports this concept, but the higher-level framework adapters (like this one) intentionally do not, for a simple reason:Keycloak, the de facto open-source standard for OIDC, doesn’t implement the
resourceparameter.Keycloak conflates OIDC clients and resource servers.
When you create a client in Keycloak, you’re effectively defining both the application and the resource server it talks to.
So, if you want your app to talk to multiple resource servers, you’ll end up creating multiple clients, e.g.:
There are two possible approaches:
Token Exchange, a standardized mechanism defined in RFC 8693, which allows
exchanging a token issued for one client or audience for another.
In practice, however, it’s implemented by very few IdPs,
Keycloak being one of the only relevant ones.
While it works, the model feels conceptually off: you still end up defining multiple clients for what is effectively a single application.
We don’t plan to support this because, despite being a standard, it’s not widely adopted and doesn’t align with our goal of remaining provider-agnostic.
Multiple OIDC instances, create one
oidc-spainstance per client.This is more standard-compliant and fully supported by
oidc-spa.It’s technically non-trivial, but already solved internally, multiple client instances can coexist in one app seamlessly, allowing communication with multiple backends.
In summary,
oidc-spa’s design is influenced by Keycloak’s interpretation of the standard, but it aims to balance practicality and correctness:It’s not the most elegant design, but it stays faithful to the standards and provides a consistent, developer-friendly experience.
If Keycloak ever supports true multi-resource semantics,
oidc-spawill evolve accordingly.Until then, multiple instances remain the recommended approach.
Example: Connecting to Vault
If your app also needs to talk to Vault (for example), you can define another OIDC instance like this:
src/getVaultToken.tsHere, we don’t specify
.withAccessTokenValidation()because it’s out of scope for our application,the Vault server itself will handle token validation (the app exchanges the access token for a Vault token).
There’s no risk of a double login here: both
myclientandmyclient-vaultbelong to the same realm (myrealm) on the same authentication server.SSO ensures that if you’re authenticated with one, you’re automatically authenticated with the other, you might just be prompted to consent to extra claims.
All reactions