Skip to content

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.user field; 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 AuthenticatedSession stub goes through a successful authenticate() 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 a ReadOnlyBucket stub 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.
Last updated · 200 distilled / 1,178 read