- A legitimate, well-behaved enterprise agent from
- A throwaway abuse-bot generated five seconds ago.
afauth-trust attestor, that a verified human is on the hook
via a per-service pseudonym sub_h (§10.4).
For new integrations, attestation is on by default. The
defineService factory in
@afauthhq/server sets unclaimed_mode: "attested_only" and wires
trustAttestor() automatically. Pass attestation: "off" to opt out
for read-only or paid-only services, or "optional" when your free tier
is cheap enough that moderation + graded limits are enough anti-abuse
(un-attested signups are allowed; a presented attestation still
validates). Which mode fits is an economic call — see When you
want attestation. See the service
quickstart.attested_only on (the defineService default, above). The agent links to a human once — afauth trust link — and then mints a fresh per-request attestation: afauth signup reads the service’s attested_only flag and auto-mints. A default agent that has never linked can’t sign up to a default service, so linking is part of the standard agent journey, not an advanced step.
When you want attestation
- Spam resistance. A bad actor spinning up 10,000 throwaway keypairs looks like 10,000 customers to your quota table. With attestation, all of one human’s agents carry the same
sub_h, sodefineServicegroups them onto one account (§10.4.4): one account, many devices — the human’s PC and phone agents share an account, like signing into one account from two devices. Bucket your free-tier quota / rate limits / bans on the account (orsub_h) and a human’s fleet draws from one bucket — no legitimate multi-device user is locked out. - Pre-claim billing. A service that wants stronger assurance that someone billable is behind the agent before granting paid features can require an attestation from a commerce attestor (
stripe-projects,fido-agent-payments,mastercard-verifiable-intent). - Enterprise compliance. A service serving B2B customers may want to limit pre-claim signup to agents running in known enterprise platforms —
microsoft-entra-agent-id,google-cloud-agent-identity,aws-iam-agent. - Rate-limit tiering. Attested agents get higher rate limits than anonymous ones; key the buckets off
sub_h.
When you don’t
Services that genuinely don’t need a human-on-the-hook signal — read-only public APIs, paid-only services where billing is the implicit anti-abuse signal — should passattestation: "off" to defineService (or use new Server({...}) without an attestor). The protocol doesn’t require attestation; only the convenience factory does.
How it works
The agent presents an attestation JWT in a header:iss— the attestor identifier (e.g.,"stripe-projects").sub— the requesting agent’s account DID.exp— token expiry (unix seconds), in the future at the time of verification.- Other claims are attestor-specific.
iss matches an accepted attestor and sub matches the request’s signing DID.
Three attestor classes
The TypeScript SDK ships three implementations ofAttestor:
| Class | Verifies | Use for |
|---|---|---|
HmacAttestor | HS256 JWTs against a shared secret | First-party service-operator attestors (you mint the token yourself) |
JwksAttestor | Asymmetric JWTs against a JWKS URL | The default trust attestor (afauth-trust); platform / commerce attestors (Stripe, Entra, etc.) |
MultiAttestor | Dispatches by iss claim | A real service that accepts multiple attestor sources |
attestor to Server; the attested_only enforcement happens automatically. Full recipe: examples/recipes/attestor.ts.
afauth-trust — the default trust attestor
Platform and commerce attestors (Entra, Stripe, etc.) are operated by third parties outside any single service’s control, which would otherwise leave attested_only unreachable from a clean v0.1 deployment. To close that gap, AFAP-0006 reserves afauth-trust as a recognized attestor identifier, operated by afauth.org at trust.afauth.org.
A trust attestation says one thing: the agent DID in sub is bound to a human-controlled account verified by the categorical method in the verification claim — one of "email", "oauth", "payment". The token carries no PII — the consuming service gets a signal, not an identity. Full model: the trust attestor.
@afauthhq/server ships a one-line trustAttestor() factory that pre-pins the AFAP-0006 issuer, JWKS URL, and algorithm. For new integrations, you don’t even need to call it — defineService wires it in automatically. If you’re using new Server({...}) directly:
afauth-trust in your discovery document’s billing.accepted_attestors. Full walkthrough: Accept afauth-trust attestations.
unclaimed_mode = "attested_only"
This is the discovery-document signal that turns on enforcement. defineService sets it by default; if you’re using new Server({...}) directly, declare it yourself:
AFAuth-Attestation header. The error response is 401 attestation_required. The account row is not created on rejection — failing closed before any side effects.
On the agent side, the afauth signup command reads this same field and auto-mints a trust attestation when present; agents that haven’t run afauth trust link are guided to do so before retrying.
Other modes ("free", "denied") don’t enforce attestation; if an agent presents one anyway, the server still validates it (rejecting invalid tokens with 401 invalid_attestation) but doesn’t require one.
Lifetime
By default, attestations are presented on a per-request basis, and the service MUST NOT cache an attestation beyond itsexp — it’s a gate on the signed request, not a session token.
Cadence is a spectrum, and most services want the middle:
- Signup-only. Check attestation once when the account is created, then trust the agent’s signature. Cheapest — but an attestor-side revoke never reaches you, the §8.5 blind spot.
- Per-request. Require a fresh attestation on every request. An attestor revoke takes effect within the token lifetime (≤ 15 min), but the attestor sits on every request’s critical path.
- Attested session (§10.7). Keep a currently-valid attestation on file and challenge with
401 attestation_requiredwhen it lapses; the agent re-presents a fresh one. Revocation takes effect within the freshness window, with no per-request attestor dependency. See Keep attested access live.
Security note
Services that accept attestations MUST validate the attestor’s signature against an authoritative key source. Stale JWKS or expired shared secrets can permit forged attestations. TheJwksAttestor from @afauthhq/server uses jose’s createRemoteJWKSet, which caches valid keys and honours key rotation. For HMAC attestors, rotate the shared secret on a schedule.
Further reading
- The trust attestor — what
afauth-trustis, what its JWTs prove, and howtrust.afauth.orgis governed. - Revocation — the two levers (attestor-side vs service-side) and which to pull when.
- Accept
afauth-trustattestations — step-by-step service-side configuration. - Spec §10 — normative attestation rules.
- Spec §9 — billing modes including
attested_only. - Spec §12.5 — threat model for compromised attestor keys.