Proposal: Token vault — upstream-IdP token storage and brokerage
Motivation
When a principal signs in through an upstream provider (ADR-0019), the provider hands back more than identity: an access token (and often a refresh token) for its APIs — the Google token that can read a calendar, the GitHub token that can list repos. Today's design discards the question; every application that wants to call the upstream API on the user's behalf must collect, store, refresh, and secure those tokens itself — N applications × M providers of scattered, long-lived credentials. Auth0 ships this as Token Vault; agentic integrations have made it table stakes. Vord should hold these tokens once, centrally, under the protections identity data already gets.
Design
-
The vault stores upstream tokens per
(principal, connection), issuance-side. Vault entries are written when a federated login or an explicit connection-link ceremony (ADR-0019) yields upstream tokens, keyed by the principal's immutable id and the upstream connection. The vault is Modgud-side state: never edge-visible, never on the verification path — rule 1 is untouched because a vault read is an explicit issuance-side operation, not token verification. -
Vault content is PII-grade: per-principal encryption, erasure included. Entries are encrypted under the principal's data key (ADR-0034), so crypto-shredding erases stored upstream tokens with everything else, and a vault row is unreadable without the principal's key. Upstream tokens are the most abusable material Vord would hold besides signing keys; they get the strongest at-rest posture the platform has.
-
Access is a token exchange, never an embedded claim. A registered, consent-gated client obtains an upstream token by presenting its Vord access token to an exchange endpoint (the RFC 8693 seat, ADR-0014) naming the connection and requested upstream scopes. Upstream tokens are never embedded in Vord-issued JWTs — they would blow the size budget (specs/vord-token-v0 §4) and turn every bearer leak into an upstream compromise. The exchange is authorized by client registration (ADR-0038) plus a consented vault scope (ADR-0037) — withdrawal cuts vault access exactly as it cuts any scope.
-
The vault owns refresh. Stored refresh tokens are used server-side to keep access tokens live; an upstream refresh failure (revoked at the provider, expired grant) marks the connection relink-required and emits an event (ADR-0022) — the application sees a clean "re-link your account" signal instead of scattered 401s.
-
Every vault access is audited. Reads, refreshes, exchange grants, and relink events are stream events; the per-principal vault view (which apps accessed which connection, when) surfaces in self-service (proposal 0010). The vault is also a named high-value asset in the threat model — its addition is exactly the "new component" trigger of ADR-0027 §4.
Alternatives considered
- Each application stores its own upstream tokens. The status quo being solved: multiplies long-lived credential copies, defeats central revocation and erasure, and makes consent withdrawal unenforceable.
- Embed upstream tokens in the Vord session or JWT. Rejected: size, and it converts every Vord bearer into an upstream bearer — the blast-radius opposite of the edge model's design.
- A generic secrets manager. Rejected: lifecycle is identity-shaped — bound to principal + connection + consent + erasure — none of which a KV-secrets engine models; proposal 0004 governs operational secrets, this governs user-delegated ones.
Graduation criteria
- A federated principal's upstream tokens are captured at link time, encrypted per-principal, and retrievable by an authorized client via the exchange — with consent enforced and withdrawal cutting access (abuse-case tested).
- Server-side refresh keeps a stored token live across the upstream expiry; a revoked upstream grant produces the relink-required event, not silent failure.
- Erasure (ADR-0034) verifiably renders vault entries unreadable.
- Vault access appears in the audit projection and the principal's self-service view.
Backward compatibility
- Additive: a new issuance-side store and one exchange grant profile on the reserved RFC 8693 seat. No token-format change; no new claim.
- The connection-link ceremony (ADR-0019) gains an optional "capture tokens into the vault" outcome, off by default per realm until enabled.
Open questions
- Hand-out vs proxy mode: return the upstream token to the client (this draft), or keep it server-side and proxy the downstream call? Proxying shrinks exposure but puts Vord on the data path — likely a per-connection policy rather than one answer.
- Scope narrowing on exchange: request a subset of stored upstream scopes, or always hand out what the vault holds?
- Whether vault-holding realms need a shorter erasure SLA than the general ADR-0034 posture, given the material's sensitivity.