Skip to main content

Why sign every request

A DFOS credential is not a bearer token. Holding one opens nothing: every credential-gated request also carries a fresh signature from your app's own key, made over that exact request. This page is the argument for why the surface is shaped that way, and what the extra signature buys.

The mechanics — the two headers, the freshness window, what each refusal status means — are in Conventions. This page is the reasoning underneath them.

The property a bearer token has

The easy design is a bearer token: sign-in hands your app a string, and the string is the authorization. Whoever presents it, wins.

Every stolen-token attack lives inside that one property. A bearer token in a log line, a crash report, a proxy cache, an error-tracking payload, or an exfiltrated database is the grant itself — not a hint about the grant, not a step toward it. The remedies are all trades: short lifetimes buy safety by throwing away durability, and revocation buys certainty back only as fast as the next check.

What DFOS sends instead

A credential-gated request carries two artifacts, and neither one is authorization by itself:

  • The credential is a signed statement that a grant exists. The user issued it, it is addressed to your app's DID, and it names an action set on one host — api:api.dfos.com, say, carrying read:profile,read:email. It says what may be done and by whom. It does not say who is asking.
  • The request proof is a short-lived signature made by your app's own key over the exact request going out: the method, the host, the path, a hash of the body, and the credential's own content identifier, right now. It says the party asking holds the key the credential was addressed to, and that this is the request they meant.

The API checks both. They only compose into authority in the hands of the party holding the key — which is the definition of proof-of-possession, and the reason the Authorization scheme token is the literal string DFOS rather than Bearer.

What a theft actually gets you

This is the payoff, and it is worth being concrete about:

What leaksWhat the holder can do with it
The credentialRead metadata: which app, which host, which actions, when it expires. Not one API call — every call needs a signature they cannot make.
A single proofReplay one request that has already happened, for about a minute. It is bound to that method, host, path, and body, so it authorizes nothing else.
The signing keyEverything the credential covers. This is the artifact that matters, which is why it lives in your server's environment and never in a browser.

The first two rows are the ones that used to be catastrophic. A credential in a log file is an embarrassment rather than an incident, and a proof captured off the wire buys a replay window measured in seconds against a request the attacker already saw the answer to.

That leaves exactly one thing to protect properly — the key — rather than a population of tokens spread across every surface a request touched.

Two layers of replay prevention

Replay is closed twice over, at two different moments, and the two are worth keeping straight:

  • At sign-in, your app mints a nonce, remembers it, and consumes it atomically when it verifies the returned artifact. A replayed callback finds the nonce already spent and is refused. The rules — how a nonce is minted, remembered, and checked, and which discipline a given scope obliges — are in SIWD § Replay prevention.
  • At request time, the proof's own freshness window and its binding to one set of request coordinates do the work. There is no nonce to track: a proof is only good for the request it already describes, and only briefly.

Why your backend signs

A browser cannot hold an Ed25519 signing key non-extractably. Any key a page can sign with is a key any script on that page can read and take away — and the key is the one artifact in this scheme whose theft is total.

So the supported shape is a backend-for-frontend: the browser holds an ordinary session cookie, and your server — which can keep a secret — signs on its behalf. The browser's job in the whole flow is to hand the credential over once, after sign-in, and then never touch it again. Key custody covers where that key lives and how to get it onto a deployed server.

Sign coordinates you wrote, not coordinates you were handed

A backend that signs whatever {method, path, body} a page asks it to has built a confused deputy: an XSS on that page, or simply a hostile client, obtains proofs for arbitrary requests against every credential the backend holds. The key never leaked, and it did not need to.

The rule that generalizes: give each call its own route with the coordinates written into the file, and let the session decide only which credential to spend. Where a parameter is genuinely necessary — a space identifier for a membership check — confine it to a named slot in a request you wrote, with a tight charset and proper encoding, never a set of coordinates you accept whole. The normative version is in API-AUTH § Security Considerations.

Why the shape generalizes

None of this is DFOS platform machinery. The user's identity is a self-verifying chain any relay can serve. The credential and the proof are byte contracts published as open specifications (CREDENTIALS, API-AUTH). Verification is a pure function of public keys, and revocation is the user's standing lever, re-checked on every request.

The resource a credential names is api:<host> — host as identifier — so any API on any domain can gate itself the same way: publish which host you are, verify the two headers, honor revocation. There is no registry to join, no authorization server to run, and no coordination with us. The DFOS API is simply the first host doing it.

Next