How the auth layer fails
Why a failure is not a signed-out person, why the session arrives with the page, why every refusal is a problem body, why a failure at a required session is the problem page, and why one party bounds each wait; and what would reverse each rule.
The auth layer is why the package is shaped as it is; hardening is what it does to resist attack. This page is the third half, split out of the first to keep both readable: what the layer does when something goes wrong, and says so in one shape.
A failure is not a signed-out person
The provider once settled on anonymous when the session read failed, on the argument that signing in
is recoverable and a spinner is not. An IdP or a session store that is down sent the person to sign
in, the sign-in failed the same way, and nobody learned what broke. fossil's /docs/design/failure —
every wait ends, every failure is seen — and this library's page put it as
one rule: a failure reaches the host whole, by its code, and within a bound.
Under the edge, three answers are kept apart on the server, where the person is read. No token
forwarded is null: nobody is signed in, and only the edge can make that so. A token that does not
verify is token/refused — a forged header, another client's token, an expired one. Keys that
could not be fetched are idp/unreachable or idp/silent, an outage, never read as a refusal: the
token may be perfectly good. jose makes the wait for the keys, so it is handed the figure rather than
wrapped in one. A plain Error, not a Problem, is a programming or deployment fault, such as
useSession outside its provider or a csp that allows 'unsafe-inline'; a code invites a catch
that routes it to a sign-in page, the wrong answer to you wired this up wrong.
What would reverse it: a product that wants an outage to look like a sign-out, such as a public
site that degrades to the anonymous view. It catches the Problem from session() and renders that
view itself; the library does not make the choice for it.
Held by packages/auth/src/server.test.ts, "names the realm unreachable, not the token refused,
when its keys cannot be fetched" and "has no session when the edge forwarded no token";
packages/auth/src/next-auth.test.ts, "throws the outage to a page that did not require a session,
which reads it itself"; packages/ui/src/failures-reach-the-host.test.ts.
The session arrives with the page
The server reads the person once per request and hands the Session to AuthProvider as a prop; the
browser reads nothing, caches nothing and renews nothing. It replaced a session read like any other
datum: a query on the host's QueryClient through the read door, read again on
focus, after a renewal and when another tab announced one on a BroadcastChannel, by a transport
(bffAuth) that renewed a session read within a minute of its expiry. All of that kept a browser's
copy of a session the browser held a cookie for. Under the edge the browser holds the edge's cookie,
the edge renews, and the next page the server renders reads the renewed token, so the copy is as old as
the page and that is enough for what it is for: drawing. @tanstack/react-query is no longer a peer of
@kanzo-tech/auth.
What it costs: a tab that stays on one page does not hear a sign-out in another tab until it navigates or calls the API, and then the edge answers for the session. Nothing it draws decides anything.
What would reverse it: a page that must redraw on a sign-out elsewhere without navigating — a long-lived dashboard showing who is signed in. Then the provider reads again on focus, and the read is a query on the host's client as the read door says.
Held by packages/auth/src/auth-provider.tsx, AuthProvider; packages/auth/src/gate.test.tsx,
"shows the fallback to an anonymous visitor, not the children".
Every refusal is a problem body
Every refusal with a code that the auth layer sends is an RFC 9457 problem: application/problem+json
with status, title and detail, and the library's code and data as extension members; and it is
the one Problem of the kernel, with its status read from the generated
registry, not from a table kept here. Under the edge the forwarder is the one part that answers a
browser's request itself, so it is the one that sends them. There were five shapes on the wire once:
routes answering { error, message }; a forwarder computing a code and sending a bodiless status, so
keasy coined bff/failed for an organization/denied the library knew; a problem page's redirect
carrying ?code=; two status tables that disagreed; and keasy's Rust server answering
application/json with no status.
The shape and why type is absent are the kernel's. One thing keeps its
shape: a required session that fails is a redirect to the product's problemPage with the code in the
URL, because a page is what a person reads. An answer about the request rather than a credential — a
cross-site request's 403, a path under no mount — has no code to name and no body.
The server speaks it too, in the same words. keasy's Rust server answers application/problem+json, and
its 401 is session/absent; the two vocabularies became one in the failure page.
A 401 without WWW-Authenticate is a stated deviation. RFC 9110 §15.5.2 says a server generating a
401 MUST send a WWW-Authenticate header with at least one challenge, and no 401 this layer sends
carries one. The browser's credential is the edge's cookie, which has no HTTP authentication scheme to
name, and a made-up scheme is a challenge no client can answer.
The forwarder names one party besides the realm: an API that does not answer is api/unreachable, 502,
and one that does not answer its headers in time is api/silent, 504
(Hardening).
Did not answer means fetch rejected with a network error, or the bound fired; an API that answers
500 has answered and its answer passes through as it came, and a client that went away is not the API's
failure. The problem carries no data naming the upstream's URL, which is the deployment's.
The reference is RFC 9457, the body fossil's problem and Problem already share.
What would reverse it: a client of the forwarder that reads OAuth's error response — error,
error_description — and cannot read a problem; none does. And a client that acts on a missing
WWW-Authenticate, which would make the challenge worth inventing.
Held by packages/auth/src/next-proxy.test.ts, "answers 401, as a problem naming session/absent,
without asking upstream when the edge forwarded no token", "answers 401, as a problem naming
token/refused, and reaches nothing, for a token that does not verify", "answers 403, as a problem naming
organization/denied, and reaches nothing, for an organization the person is not a member of", "answers
502, as a problem naming api/unreachable, when the API refuses the connection" and "passes an API's own
500 through, because an answer with an error status is still its answer". The server's problem answer
is to be held by a test in the server's own suite and by the generated table's round trip.
A failure at a required session is the problem page
kanzoAuth().session({ required: true }) answers a refused token or an outage reading the realm's
keys with a redirect to problemPage and the code, and a missing token with a redirect to the edge's
sign-in, back to the page. Without it the outage would be thrown raw, and keasy wrapped every
layout's read in a try that redirected to the same page with the same code: the library's own
decision written again by the product. A refused token goes to the problem page and not to sign-in,
because the edge holds a session and would forward the same token: that is a loop. Without required
the failure is still thrown, for a page that renders through one. TenantRequest, the type
KanzoAuthConfig.organization takes, is on ./next with it. The reference is AuthKit's
withAuth({ ensureSignedIn: true }), which settles the outcome rather than handing the host an
exception to settle.
What would reverse it: a layout that must render through an outage. Then it does not ask for
required, and catches the Problem itself.
Held by packages/auth/src/next-auth.test.ts, "sends a required session's outage to the problem
page, with its code", "sends a required session's refused token to the problem page, with its code"
and "throws the outage to a page that did not require a session, which reads it itself".
One party bounds each wait
The package makes two waits and bounds both: the realm's keys, 10 s, handed to jose's own timeout
as idp/silent; and the forwarder's hop to an API, 25 s until its headers, through the kernel's
within as api/silent. A host bounds its own API calls in the requests it makes. The rule is
the failure page's: the party that makes the request
bounds it, and where a library makes it, the library is handed the figure. The browser half makes no
request at all since the session became a prop, so the 30 s bound on its session read went with it.
What would reverse it: a host that cannot bound its own requests — one whose API client takes no signal. Then the package would need a transport of its own in the browser again, which the edge made unnecessary.
Held by packages/auth/src/forwarded.ts, KEYS_WITHIN; packages/auth/src/next-proxy.test.ts,
"answers 504, as a problem naming api/silent, when the API sends no headers within the bound".
The auth layer
Why authentication is a sibling package rather than a component, why the relying party is the platform's oauth2-proxy and not this package, why a browser draws instead of deciding, and why the forwarded token is verified on every read.
Hardening the auth layer
What @kanzo-tech/auth does so that a script on the origin, a hung API, a request with no origin or a forged header does not become an API call; what moved to the platform's edge; the one session reader; the realm baseline; and the limits it states instead of fixing, each with what would reverse it.