PATTERN Cited by 1 source
Capability-returning authenticate()¶
Problem¶
Authenticating a client at the start of an RPC session and authorizing its subsequent calls is traditionally done in one of two ways, each with real ergonomic and security cost:
- Re-present credentials on every call. The client includes an API key / bearer token in every request; the server re-validates on every method. Works, but verbose — and means every handler has a shared "have you authorized?" preamble which is easy to forget on a new route.
- Connection-state flag mutated by a special "login" message.
An "authenticate" RPC mutates a hidden
connection.userfield; subsequent calls check that field. This is the classical WebSocket shape (headers can't be added after the handshake, so the auth message must be in-band). The "authenticate" message is special — it changes the state of the connection in a way that breaks the RPC abstraction and is easy to forget / double-call / get wrong.
Both patterns put the authorization check in the server handler, not in the type system.
Pattern¶
On an object-capability RPC
system, expose authenticate(credential) as a top-level method
that returns an authenticated session object by reference
(not a boolean, not a token). The caller receives a stub. All
subsequent protected methods are defined on the session class —
not on the top-level API — so the client can only invoke them
through the returned stub.
// Top-level API: anyone can call authenticate.
class ApiServer extends RpcTarget {
async authenticate(apiKey: string): Promise<AuthenticatedSession> {
const username = await checkApiKey(apiKey) // may throw
return new AuthenticatedSession(username)
}
}
// Returned by authenticate(); only callers who successfully
// authenticated hold references.
class AuthenticatedSession extends RpcTarget {
constructor(private username: string) { super() }
whoami(): string { return this.username }
// ... all other methods requiring auth
}
// Client code:
const session = await api.authenticate(apiKey)
const name = await session.whoami()
The returned session stub is unforgeable at the protocol
layer. The client cannot construct one out of thin air — the
only way to get one is to call authenticate() and have it
return successfully. The authorization check is encoded in the
type system: you can't call session.whoami() without a
session, and you can't get a session without authenticating.
Round-trip collapse via promise pipelining¶
The authenticate(key).whoami() pattern is two dependent calls,
but under promise pipelining the client does not wait for
authenticate() to return before issuing whoami():
const sessionPromise = batch.authenticate(apiKey)
const name = await sessionPromise.whoami() // pipelined
Both calls ship in one network round trip. The server sees: "push: authenticate(…); push: invoke whoami() on the result of push #1; pull: return the result of push #2" — and replies once. This makes the capability-returning idiom cheaper than the classical retransmit-credentials pattern, not more expensive.
Why it works¶
- Unforgeable references. Capability RPC makes stubs first-
class wire values; the client cannot synthesize one. The only
path to obtaining an
AuthenticatedSessionstub goes through a successfulauthenticate()call. - Type-safe authorization. Callers cannot forget the auth check because the compiler won't let them call protected methods without a session object.
- No connection-state side effects.
authenticate()is a normal method returning a normal value. It does not mutate hidden state, it does not have to be the first call, it is not a protocol-level special case. - Works over any bidirectional transport. WebSocket, HTTP
batch (within one batch),
postMessage()— all support the same semantics, because the auth contract lives in types, not in transport hooks. - Composes. Attenuated capabilities are just narrower
returned interfaces:
authenticateReadOnly(key)returns aReadOnlyBucketstub that simply doesn't have write methods. Least privilege is what interface did I return, not what ACL did I attach.
Known uses¶
- Cap'n Web (Cloudflare, 2025) — named as the canonical idiom in the announcement post; Varda walks the exact pattern above. "It is impossible for the client to 'forge' a session object. The only way to get one is to call authenticate(), and have it return successfully."
- Cap'n Proto (Cloudflare / Sandstorm, ~2013) — the ancestor where this pattern was first normalized.
- Cloudflare Workers RPC (JavaScript-native RPC between Workers services) — uses the same idiom for inter-Worker auth.
Trade-offs¶
- Requires capability RPC. The pattern doesn't translate to gRPC / JSON-RPC / classical REST because those RPC systems have no notion of returning an object-by-reference whose method invocations route back to the server. You would have to return a session token and re-present it on every call — which collapses back to the "re-present credentials" antipattern (the token is the credential).
- Connection lifetime matters. The session stub is valid only as long as the connection. Long-lived applications need a reconnect-and-re-authenticate flow; Cap'n Web's HTTP batch mode explicitly breaks stubs at batch end.
- Session GC / revocation. Revoking a session means invalidating the stub. Capability RPC runtimes need explicit disposal hooks (or a parallel revocation mechanism) — the "just drop the token" pattern of JWTs does not apply.
- Harder to debug in tcpdump. Traditional auth leaves the bearer token visibly on every request; capability auth leaves an integer export-table ID that is only meaningful in the context of the full session. Tracing requires stub correlation.
- Does not subsume authentication-across-systems. Tokens (JWTs, Macaroons, API keys) still cross trust boundaries. Capability returns optimise authorization within a capability- RPC session; federated / cross-service authn needs the token system too.
Related¶
- concepts/object-capability-rpc — the prerequisite.
- concepts/promise-pipelining — what makes the idiom round- trip-efficient.
- concepts/bidirectional-rpc — same underlying mechanism.
- patterns/record-replay-dsl — another capability-RPC idiom from the same Cap'n Web post.
- systems/capnweb / systems/capnproto