Overview / Identity / auth
ADR 0028
Service-to-service authentication uses three schemes, chosen per trust boundary
AcceptedIdentity / auth
Context
commerce-api sits between commerce-risk (the risk/KYC decision service, ADR 0024) and commerce-payments/the CDE (the money control plane, ADR 0006/0009), and commerce-risk also calls back into commerce-api. Each of these boundaries has a different threat model — a read-only verdict lookup, a money-moving request, and a low-sensitivity inbound lookup — so a single uniform scheme would be either over- or under-fit somewhere.
Decision
Use three different service-to-service authentication schemes, one per boundary, each matched to that boundary's integrity, replay, and latency needs. No mTLS or service mesh.
- Outbound → commerce-risk: a short-lived HS256 service JWT. Minted per call with
iss=commerce-api,aud=commerce-risk, a scope claim, andexp = now + 60s; verified by risk's service-auth guard. Audience-binding + auto-expiry suit a read-only verdict lookup. - Outbound → commerce-payments / CDE: HMAC-SHA256 request signing. A canonical
timestamp.nonce.METHOD.path.sha256hex(body)string (dot-separated, body hashed) is signed and sent asx-key-id/x-timestamp/x-nonce/x-signature. Per-request body integrity + nonce replay-resistance is the right bar for money movement. - Inbound ← commerce-risk (disputes): a static shared bearer. A
DISPUTE_LOOKUP_TOKEN, constant-time compared, on the otherwise-unauthenticated internal pipeline. This began as a low-sensitivity txn lookup, but the same bearer also guards money-moving dispute endpoints —settle(enqueues the chargeback debit) andhold/release(change available balance) — which is a weaker mechanism than those actions warrant (see to-revisit).
Consequences
- Positive: the JWT and HMAC boundaries fit their threat models — expiring audience-bound tokens for the verdict read, signed bodies with nonces for money moves.
- Neutral / to revisit — the static dispute bearer is under-scoped for what it now guards. It began as a lookup credential but also authenticates
settle/hold/release, which move money — a plain shared bearer with no per-request integrity or expiry is weaker than those actions deserve. Upgrading that boundary (HMAC or a scoped short-lived token) is a follow-up. - Neutral / to revisit: three secret types to provision and rotate, and no single revocation story; trust rests on shared secrets in each service's environment, so a leaked secret is a boundary compromise until rotated.
- Neutral / to revisit: there is no mTLS/service-mesh identity layer, so the schemes are application-level; even the "internal" inbound route is explicitly authenticated rather than trusting the private network.
Alternatives considered
- One uniform scheme everywhere (a single shared JWT or bearer): rejected — it would be over-fit for the cheap inbound lookup and under-fit for money movement (no per-request body integrity).
- mTLS / a service mesh: rejected — infrastructure weight not justified for a handful of service-to-service endpoints; application-level schemes cover the needs today.
- Trust the private network with no per-request auth: rejected — even the internal inbound route is authenticated, so a foothold in the network is not a free pass to the money boundary.