# Whose request is this? Delegated identity between services

Author: Chris Chabot
Canonical URL: https://chabot.dev/blog/whose-request-is-this-delegated-identity-between-services/
Published: 2026-11-03

Keeping the calling service separate from the user behind it: absent, invalid and verified identity, binding claims to the resource, and what replay memory needs.

A shared service credential answers one question: which service is calling? It does not, by itself, answer whose work the service is doing or which resources that person may use. My work on service SDKs and delegated authorization has included keeping those questions separate, both in the request envelope and in the checks that consume it. That separation matters most when verification produces something other than a clean yes.

Consider a service that accepts requests from a trusted gateway and an optional token describing the user behind each request. It is tempting to reduce verification to two outcomes: a delegated identity, or no delegated identity. If the second outcome grants full service access, however, an invalid token can become a route to more authority than a valid one. Nothing has to defeat the signature algorithm. The error branch has already done the damage.

The useful model has three states: absent, invalid and verified. Invalid means the request presented a claim it could not prove, and the service refuses it. Verified means authorization can proceed using the claims that passed verification. Absent needs its own policy: perhaps this method permits a service-only caller, perhaps it requires a user, but a failed verification must never be converted into the absent case. A boolean is too small to hold that distinction.

The request envelope follows the same separation. A service credential identifies the immediate caller. A delegated token carries the identity and authority being forwarded, including a tenant, a user, scopes and, where the work requires them, task or resource bindings. Issuer, audience, expiry and a token ID describe where that token came from and where it may be used. A per-request nonce distinguishes one use from another. After verification, handlers receive a context containing the verified identity and claims rather than reconstructing authority from convenient request fields.

Verification has to check more than the signature. The issuer and audience must be the expected ones, the token must be within its validity period, and the required identity and scope fields must be present and well formed. Audience binding is the idea behind [RFC 8707's resource indicators](https://www.rfc-editor.org/rfc/rfc8707.html): a token intended for one service should not become a credential for every other service that happens to trust the same issuer. A token with a valid signature can still be the wrong token for this request.

The next check binds those claims to the operation. Imagine a storage service that has a default workspace in its configuration but also accepts a workspace in each request. Authorizing the configured workspace and then reading from the requested one proves the wrong thing. The authorization check must use the resource that the operation will actually touch, comparing it with the verified tenant, resource claims and scope. The same reasoning applies to a task-bound request: the task named in the token must match the task named in the operation. Omitting the requested ID must not provide a way around a required match.

A connection can supply another part of the identity. If a replication connection has authenticated one client, a write-back message must not be allowed to rename that client merely by carrying a different ID in its payload. The receiver can derive the origin from the authenticated peer and reject a conflicting message. This is the same rule at a different boundary: use the identity that was verified, then check the data against it.

Replay needs separate state. A signature proves that a token was issued; it does not prove that this request has not already been handled. A verifier can combine the token ID with a request nonce, reject a pair it has seen before and retain that pair until its validity window ends. The key components need bounded, unambiguous encoding so a crafted token ID or nonce cannot collide with another pair. The verifier should check the credential, claims and request binding before marking the pair as seen, so an unverified request cannot consume another request's replay slot.

Where that memory lives decides what it guarantees. A mutex around an in-process map can make the check atomic within one process. It cannot tell another replica what has happened, and a restart forgets the map. Pruning expired entries is consistent with the token's validity window; evicting an unexpired entry to meet a size cap can allow a replay that the process previously knew about. If the requirement is to refuse reuse across replicas and restarts, a process-local cache does not meet it. That is a storage and coordination decision, not another signature check.

Configuration belongs in this model too. A server that has no verifier needs an explicit answer for requests that carry credentials, and a warning at startup is not a refusal. Anonymous access may be deliberate, but it should be a named mode with its own permitted operations, not the accidental result of leaving a key unset. Likewise, an empty scope list and an unrestricted scope must not become synonyms merely because the implementation uses “nothing to check” as its default. Optional claims are only useful when every caller and verifier agrees on what their absence means.

I also worked on making authentication rejection observable in the shared SDKs. A structured rejection record can name the server, method, peer and error code without logging the credential itself. Keeping authentication failures distinct from successful calls and unrelated application errors makes it possible to see a broken identity boundary without burying it in ordinary traffic. Observability does not supply missing enforcement, but it makes an unexpected fallback much harder to overlook.

The signature is one part of this chain. The rest is deciding which identity to trust, which resource to compare, when a request counts as spent and what missing configuration means. Those decisions belong in explicit states and checks close to the operation they govern. A fallback is a decision too, made by whoever wrote the else branch, and it tends to be generous.
