ProofedMesh/docs/adr/0002-session-scope-token-format.md

6.2 KiB
Raw Blame History

ADR-0002: Session-Scope-Token-Format

  • Status: accepted
  • Datum: 2026-10-25
  • Entscheider: CTO
  • Betroffene Dokumente: docs/architecture/v1.1.md Abschnitte 3c, 4.5a, 5.1, 6, 7a.0, 9.1

Kontext und Problemstellung

Ein Agent, der eine Datenquelle liest, muss ihr beweisen, dass er für Benutzer Y in einer aktiven Session S liest — nicht nur, dass er irgendein Agent ist. Ohne diesen Nachweis gibt es keine saubere Grundlage für OPA-Regeln wie „Agent-Travel darf nur project_assignments seines eigenen Benutzers lesen, nicht fremder". Pocket ID allein liefert nur Agent-Identität (via client_credentials), keine Benutzer-Delegation.

Entscheidungsoptionen

  • Option A — Vollständiger OAuth 2.0 Token Exchange (RFC 8693) in Pocket ID. Agent macht Token-Exchange gegen Pocket ID; dieser prüft Session und mintet ein Delegations-Token mit act-Claim.
  • Option B — Eigener Session Scope Token Service (SSTS) in trust-api. Agent präsentiert Pocket-ID-Access-Token + K8s-SA-Token; SSTS prüft doppelt, konsultiert OPA, stellt kurzlebigen JWT mit act-Claim aus, signiert mit eigenem Ed25519-Schlüssel. Datenquellen-Adapter validieren per JWKS offline.
  • Option C — Opaque Bearer Tokens mit zentralem Introspection-Endpunkt. Datenquellen rufen bei jedem Request POST /introspect am trust-api.

Entscheidung

Option B. Der Session Scope Token Service läuft als Modul in trust-api und stellt JWTs im Format gemäß Anhang A dieser ADR aus, signiert mit einem eigenen Ed25519-Schlüssel.

Begründung

  • Pocket ID macht keinen RFC-8693-Flow. Der Check im Pocket-ID-OIDC-Discovery zeigt client_credentials, aber keinen urn:ietf:params:oauth:grant-type:token-exchange. Option A würde Pocket ID erweitern müssen oder einen zweiten IdP aufbauen — beides nicht gewünscht (siehe docs/architecture/v1.1.md 3a).
  • Offline-Validierung ist wichtig. Datenquellen-Adapter sollen Tokens ohne Round-Trip zum trust-api prüfen können — JWKS + signierter JWT reichen. Option C erzwingt eine Netzwerkrunde pro Lese-Request und bindet Datenquellen-Verfügbarkeit an trust-api-Verfügbarkeit.
  • OPA-Entscheidung bleibt zentral. Der SSTS konsultiert OPA genau einmal beim Ausstellen, nicht pro Query. Rego-Policies müssen nur im trust-api deployed sein.

Entscheidung — Anhang A: Token-Format

Header:

{ "alg": "EdDSA", "typ": "JWT", "kid": "sst-2026-q4" }

Payload (Claims):

{
  "iss": "https://trust.basicstack.de",
  "sub": "agent-travel",
  "act": {
    "iss": "https://auth.basicstack.de",
    "sub": "user-12345",
    "session_id": "SES-2026-10-25-abc"
  },
  "aud": "ds:expense-db-app",
  "scope": {
    "data_source": "ds:expense-db-app",
    "resource": "project_assignments",
    "filters": { "employee_id": "EMP-12345" },
    "operation": "read"
  },
  "jti": "01HE2D3V6X7Y8Z9ABCDEFG",
  "iat": 1735120000,
  "exp": 1735120300,
  "nbf": 1735120000,
  "policy_version": "sha256:<rego-bundle-digest>"
}

Semantik:

  • iss = feste trust-api-Issuer-URL.
  • sub = agent_id (OIDC-Client-Name in Pocket ID + Eintrag in Agent Registry).
  • act = Delegations-Kontext nach RFC 8693 §4.1 (Semantik, nicht der volle Flow). act.sub ist der Benutzer, act.session_id die Approval-Service-Session.
  • aud = genau eine Zieldatenquelle. Mehrere Datenquellen → mehrere Tokens.
  • scope = engster erlaubter Scope. Datenquellen-Adapter MUSS scope strikt gegen Request matchen (keine erweiterten Filter, keine zusätzlichen Resources).
  • jti = ULID oder UUID. Datenquellen-Adapter führen scope_token_log mit UNIQUE (data_source_id, jti) als optionalen Replay-Schutz zusätzlich zur exp-Prüfung.
  • exp − iat = ≤ 300 Sekunden (5 Minuten). Begründung: Agenten holen Tokens pro Query, nicht pro Session.
  • nbf = iat.
  • policy_version = SHA-256 des zur Entscheidungszeit aktiven OPA-Rego-Bundles. Wandert später in die Query Attestation für Nachvollziehbarkeit.

Signatur: Ed25519. Schlüsselmaterial liegt in OpenBao Transit (transit/keys/session-scope-token-signer), rotiert alle 90 Tage. JWKS unter https://trust.basicstack.de/.well-known/jwks.json.

Begründung Detailentscheidungen

  • EdDSA statt RS256. Konsistent zum restlichen Signatur-Stack (DSSE / in-toto nutzen Ed25519).
  • 5-Minuten-TTL. Lang genug für Chat-Interaktionen mit einer Datenquelle; kurz genug, dass ein geleakter Token begrenzten Schaden anrichtet. Agenten holen bei Bedarf einen neuen; /session-scope-tokens ist bewusst billig.
  • scope.filters ist Teil des Tokens. Alternative wäre „offene" Reads mit späterer Filterprüfung — würde aber erlauben, dass ein Agent erst eine breite Query stellt und dann nur bestimmte Zeilen weitergibt. Mit Filter im Token geht das nicht; die Query Attestation beweist zudem die gefilterten Parameter.
  • aud = ds:.... Explizite Audience bindet den Token an eine Datenquelle. Verhindert, dass ein Token bei anderen Datenquellen „probiert" wird.

Konsequenzen

Positive

  • OPA-Policies werden einmal pro Scope-Request ausgewertet, nicht pro Query-Zeile.
  • Datenquellen können offline (ohne trust-api) authorisieren — bessere Entkopplung, besseres Fehlerverhalten.
  • Delegationskette ist in Attestierungen vollständig nachvollziehbar: session_scope_token_jti zeigt zurück auf Token-Ausstellung, die wiederum OPA-Policy-Version trägt.

Negative / Trade-offs

  • Zusätzliche Signatur-Round-Trip (Agent → /session-scope-tokens) pro Lese-Operation. Bei chattigen Workflows mit vielen kleinen Queries ggf. relevant — eine spätere ADR könnte Scope-Token mit mehreren erlaubten Resources diskutieren.
  • Datenquellen-Adapter brauchen JWT-Validierungs-Bibliotheken (für Go: trivial, github.com/go-jose/go-jose).

Folgearbeiten / Open Points

  • Client-Library (Go) für Agenten mit Caching eines Scope-Tokens bis exp - 30s.
  • Rego-Testsuite für die drei Policy-Klassen (Transformation, Senke, Datenquelle).
  • Monitoring: Rate von Scope-Token-Ausstellungen pro Agent/Benutzer; Anomalie-Erkennung.
  • docs/architecture/v1.1.md Abschnitte 3c, 4.5a, 5.1, 6, 7a.0, 9.1
  • RFC 8693 (OAuth 2.0 Token Exchange)
  • Pocket ID OIDC Discovery: https://auth.basicstack.de/.well-known/openid-configuration