Service-to-service auth
Internal routes (everything under /internal) accept a call only when it carries a valid service token: a short-lived signed JWT in the Authorization: Bearer header. Nothing else is accepted, not cookies, not query strings.
The token
| Claim | Meaning |
|---|---|
typ | Always service. |
iss | The calling service, for example spark-backend, spark-flows, spark-launch. |
aud | The service being called, for example spark-launch.internal, spark-meta.internal, spark-tiktok.internal, spark-backend.internal. A single string, never a list. |
iat, exp | Issued-at and expiry. Lifetime is at most 60 seconds, and both must sit around "now" (a few seconds of clock skew are allowed). |
scope | Backend tokens only: org (a client acting on its own organization) or system (Spark staff). |
org | The organization, required when scope is org. |
actor | Who acts: { kind, id } where kind is user, admin, agent, service, policy or system. |
perms | Backend tokens only: the caller's permissions, for example launch.read, strategy.approve. |
The signing key is shared only between the services that need it and lives in each service's private configuration. It is never logged, and never part of a response.
Issuer allowlists
Each mount declares which issuers it accepts. In spark_launch:
- every client and staff route accepts only
spark-backend; GET /internal/launches/:orgId/readinessalso acceptsspark-flows. Flows tokens carry no scope or permissions; any they send are ignored.
When spark_launch calls out (to read platform snapshots or ask the backend for AI text), it signs its own token with iss: "spark-launch" and the callee's aud.
Scope and permissions
- The backend checks the user's session and permission before it proxies a call.
spark_launchchecks again from the token (defence in depth):- Org scope: the organization is taken only from the token's
orgclaim. Any organization id in the path, query or body is ignored. - System scope: the organization is the
:orgIdpath parameter. - A missing permission answers
403, except on document routes, which answer404so their existence is not revealed. - Some routes are org-only (confirming AI assumptions, approving, withdrawing): staff tokens are refused with
403 client_only, even though they carry every permission. - Some routes are system-only (staff notes, document access log, evidence upload): org tokens see
404.
- Org scope: the organization is taken only from the token's
Failures
A rejected token always answers 401 { "error": "unauthorized" }. The reason is logged on the server, never the token itself.