Auth
Keycloak's claims read into one Session, a role predicate that knows about organizations, and a Backend For Frontend for Next in one object. A sibling of @kanzo-tech/ui, never part of it.
@kanzo-tech/auth is what a product needs once there is somebody signed in: the claim vocabulary
read into one session, the role evaluation that knows about organizations, and the Backend For
Frontend that signs people in, keeps their session alive and ends it — kanzoAuth for Next.
It is a sibling of @kanzo-tech/ui, not part of it. It does not depend on the library, and
nothing in the library may depend on it.
The one rule to read before anything else
What a client knows about its roles is for drawing, never for deciding.
The roles in a session are a copy, and a copy is something an attacker owns the moment it reaches
the browser. Gate hides a control and can answers a question about what to draw; the resource
server, validating the access token it was sent, is what refuses the request behind the button. A
product gated only here is ungated.
The same rule one level down: never open the access token in the client. It is opaque to a client by definition, its format is not guaranteed to stay stable, and it may be encrypted for the resource — which is why depending on its contents is, in Microsoft's words for exactly this mistake, "one of the most common sources of errors and client logic breakage." Roles for drawing come from the session; the session comes from the ID token or from your own server.
This is physical rather than a preference, so there is no configuration that changes it and no future release that softens it. Every other page assumes it and points back here rather than arguing it again; the auth layer is why it is the first thing this page says, and what else follows from it.
Why it is not in the library
The first admission rule is domain-free — nothing about RDF / SHACL /
fossil / graphs / auth. Auth is excluded by name, and there is a second, blunter reason that
the root barrel makes structural: a consumer who wants a Button must not pay for a session.
The auth layer has the composite that made the rule, and why this package
depends on nothing of the library's.
What it is not
There is no sign-in screen, no user menu and no organization switcher. A sign-in screen is a
logo, a legal line, a privacy notice and a button, and every product answers those differently —
@kanzo-tech/ui has the parts to draw all three, and the auth
layer is why none of them is shipped from here.
There is no protocol in it either. The confidential client belongs to openid-client, behind
./server, and the browser holds a cookie and no token — RFC 10017's Backend For Frontend, the
architecture it recommends for business applications. Writing OAuth by hand is where a mistake
stops being a bug and becomes a vulnerability.
What is left after those two subtractions is the whole of the package: Keycloak's claims as one
Session, a role predicate that understands organizations, and the session lifecycle around them —
renewed and ended on the server, signed back in once in the browser.
Adding auth to a product
The server half is one object and four files — Next.js has them in full:
import { kanzoAuth } from "@kanzo-tech/auth/next";
export const auth = kanzoAuth(async () => ({ clientId, clientSecret, issuer, secret, store }));export const proxy = (request: NextRequest) => auth.proxy(request);export const { GET, POST } = auth.routes;The browser half is one line and a provider:
import { AuthProvider, bffAuth } from "@kanzo-tech/auth";
const auth = bffAuth();
<AuthProvider auth={auth}>
<App />
</AuthProvider>;// Every request from here on carries the session, renews it once, and signs in when it is over.
createClient<paths>({ baseUrl: "/", fetch: auth.fetch });import { Gate, useSession } from "@kanzo-tech/auth";
const { session, can } = useSession();
<Gate role="warden">
<Button>Post a contract</Button>
</Gate>;bffAuth is built outside React, on purpose. A product's API client is a module rather than a
component, and a fetch reachable only from a hook would drag every call site into the tree to get
at it.
What a name means here
The shape of a name says what it gives you, and there are two families:
| Shape | What it answers | Names |
|---|---|---|
<where>Auth | a whole side of the BFF in one object | bffAuth, the browser's Auth; kanzoAuth, the server's |
| anything else | itself | relyingParty, issuer, sealedCookie, claims, can |
The suffix is a promise, so the second family is not a leftover: the relying party answers the
protocol's verbs — begin, complete, read, refresh, end — and a name ending in Auth
would promise a side of the pattern it is only a part of. Relying party is the term the
specification already uses for it.
Who is signed in
useSession is the hook, and status is the field to branch on — not session === null, which is
the same answer for anonymous and we have not looked yet. Drawing a sign-in prompt during the
second is the flicker every application with a session has shipped at least once. A session that
could not be read is a fourth answer, "failed", with what was thrown on error — never
anonymous, which would send the person to sign in over an outage.
Every example on these pages runs against a fabricated session rather than a realm, from one file —
docs/lib/guild-auth.ts, which builds a claim set and runs the package's own reader over it, so
what they draw is what a token would produce rather than a Session somebody typed. Auth is five
members — getSession, subscribe, signIn, signOut and a fetch — which is what makes faking
it possible at all.
What the token actually carries puts that claim set and
the Session it becomes side by side, with the reading client id as a control: the machinery is on
screen rather than described.
Installing it
pnpm add @kanzo-tech/authThe root barrel carries no engine — react and nothing else — which is why the browser half
of every product imports it.
| Door | What it is | Peer to install |
|---|---|---|
@kanzo-tech/auth | Types, the claim reader, can, the provider, the hooks, Gate and bffAuth | none |
@kanzo-tech/auth/server | relyingParty — the confidential client: exchange, sealed cookie, renewal, end-session, back-channel logout — and the session stores | openid-client, jose |
@kanzo-tech/auth/next | kanzoAuth — the proxy, the routes, the forwarder and the server component's session | openid-client, jose, next 16 |
A door exists here only because it imports an engine, which is the engine rule the library's own subpaths follow. A consumer installs the peer for the door they open and no other.
pnpm add @kanzo-tech/auth openid-client joseThe two Node doors never reach React: they import the modules under the root barrel directly, so a server process does not pull a component library in behind an import of an OAuth client.
The realm it expects
The identity service is the server half — Keycloak with its realm as code —
and it ships from this repository under the same tag as the package. What this package needs from
it is small and none of it is invented: role claims on both tokens, an audience mapper naming your API, and the
organization:* scope. Keycloak is that page, and it is the one to hand to
whoever runs the realm.
Session
The shape, bffAuth, how a session is kept alive and ends, and the current organization.
Roles
can, Gate, useSession().can, and where the roles inside an organization are read from.
Keycloak
The realm a consumer needs: two token toggles, an audience mapper, and the scope with the star.
The identity service
Keycloak with the realm as code: run it, register an application, check the contract.
Server
The confidential client: strings in and strings out, a stable ticket renewed in place, back-channel logout.
Next.js
kanzoAuth: the BFF as one object, in the four files an App Router product already has.
The gateway
The server half of @kanzo-tech/llm — LiteLLM as the platform's one door to models, reached by alias through an application's own server, with a team and a key per tenant.
Session
One session shape in the browser, bffAuth behind it, how a session is kept alive and how it ends, and why the current organization is resolved per request rather than stored.