# Gateway Identity Propagation Contract (v1)
`road-runner-common` is the single source of truth for authentication/authorization across all
Rust services. Authentication happens **once, at the edge gateway**; downstream services trust
the gateway and never re-validate tokens. The gateway forwards the resolved identity as signed
`x-auth-*` headers, and each service builds a [`UserContext`] from them.
## Trust model
- The gateway is the only ingress. It **must strip** any client-supplied `x-auth-*` headers and
re-emit its own.
- Defense-in-depth: the gateway **signs** the identity headers with a shared HMAC secret. Services
verify the signature (`gateway::gateway_resolver`), so a spoofed header that bypasses network
isolation is still rejected.
- Network isolation (mTLS / NetworkPolicy) remains the first line of defense; the signature is the
second.
## Headers (gateway → service)
| `x-auth-principal-type` | no | `user` \| `apikey` \| `service` (default `user`) |
| `x-auth-user-sub` | yes* | canonical subject; user UUID (owner UUID for apikey) |
| `x-auth-user-email` | no | email |
| `x-auth-user-username` | no | username |
| `x-auth-roles` | no | comma-separated roles |
| `x-auth-scopes` | no | comma-separated API-key permissions (Core catalog) |
| `x-auth-capabilities` | no | comma-separated account capabilities (entitlements) |
| `x-auth-apikey-id` | apikey | API key id |
| `x-auth-issued-at` | yes** | unix seconds (replay protection) |
| `x-auth-signature` | yes** | lowercase hex HMAC-SHA256 of the canonical string |
\* Without `x-auth-user-sub` the request is treated as anonymous (the per-service `required` flag
and the per-endpoint `UserContext` extractor decide whether that is allowed).
\** Required whenever signature verification is enabled (the default).
## Canonical signing string (v1)
Fields joined with `\n`, missing optionals are the empty string. `roles`/`scopes`/`issued_at`
are the **exact** raw header values, so the gateway must build this identically:
```text
v1
<sub>
<principal> # "user" | "apikey" | "service"
<email>
<username>
<apikey_id>
<roles> # raw csv
<scopes> # raw csv
<capabilities> # raw csv
<issued_at>
```
`signature = lowercase_hex( HMAC_SHA256( secret, canonical ) )`
## Service configuration
| `AUTH_GATEWAY_HMAC_SECRET` | — | shared secret (required when verifying) |
| `AUTH_GATEWAY_VERIFY_SIGNATURE` | `true` | fail closed: reject missing/invalid signatures |
| `AUTH_GATEWAY_MAX_AGE_SECS` | `300` | replay window for `x-auth-issued-at` |
Verification on with no secret is a hard startup error (fail closed).
## Usage in a service
```rust
use road_runner_common::gateway::{gateway_resolver, GatewayAuthConfig};
use road_runner_common::middleware::UserContextMiddleware;
let cfg = GatewayAuthConfig::from_env()?; // in main()
let auth = UserContextMiddleware::new(gateway_resolver(cfg)).required(false);
// `.wrap(auth)` on the App. Public routes (health, swagger) stay open;
// user handlers take `user_ctx: UserContext` and 401 automatically when absent.
```
Services with their own async identity resolution (e.g. `identity-account`, which reads Redis
sessions) keep their own middleware but **build the same `UserContext` type** — the synchronous
`gateway_resolver` is for the common stateless case.
## API-key permissions (Core catalog)
API keys are member-scoped and self-service. A member picks permissions from a fixed catalog
(`road_runner_common::permissions`): `read`, `spot`, `derivatives`, `wallet_transfer`,
`withdraw`. A key with only `read` is a BEARER (hash-verified) key; any write permission makes it
an HMAC (request-signed) key. `withdraw` requires an IP allowlist on the key.
## Authorization helpers
```rust
user_ctx.require_role("trader")?;
user_ctx.require_any_role(&["admin", "ops"])?;
user_ctx.require_scope("spot")?; // API-key permission
user_ctx.require_admin()?; // ADMIN or *_ADMIN
```