Skip to content

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 ​

ClaimMeaning
typAlways service.
issThe calling service, for example spark-backend, spark-flows, spark-launch.
audThe service being called, for example spark-launch.internal, spark-meta.internal, spark-tiktok.internal, spark-backend.internal. A single string, never a list.
iat, expIssued-at and expiry. Lifetime is at most 60 seconds, and both must sit around "now" (a few seconds of clock skew are allowed).
scopeBackend tokens only: org (a client acting on its own organization) or system (Spark staff).
orgThe organization, required when scope is org.
actorWho acts: { kind, id } where kind is user, admin, agent, service, policy or system.
permsBackend 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/readiness also accepts spark-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 ​

  1. The backend checks the user's session and permission before it proxies a call.
  2. spark_launch checks again from the token (defence in depth):
    • Org scope: the organization is taken only from the token's org claim. Any organization id in the path, query or body is ignored.
    • System scope: the organization is the :orgId path parameter.
    • A missing permission answers 403, except on document routes, which answer 404 so 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.

Failures ​

A rejected token always answers 401 { "error": "unauthorized" }. The reason is logged on the server, never the token itself.

Spark help center. Questions? Email info@sparkautoflow.com.