You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Surface mission_id — a stable, opaque identifier for a delegation tree — as a first-class, queryable field: a JWT claim, a column on issued_credentials and cae_signals, a log field, and an admin API filter. Enables efficient workflow-level audit queries without walking the parent_jti chain at read time.
Implementation note:mission_id is currently populated with the JTI of the root credential in the delegation tree. Consumers MUST treat it as opaque — do not assume the value is a JTI, do not attempt to look up the underlying credential by this ID. Keeping the value opaque lets the population scheme evolve (e.g., to match a future standardized claim) without breaking consumers.
Motivation
CoSAI Agentic IAM Appendix D's "prove control on demand" query list includes "Retrieve events sharing the same correlation_id, ordered by delegation depth" — reconstruct a full workflow end-to-end. ZeroID's existing parent_jti links let you walk this chain, but every workflow query becomes an O(depth) recursive CTE.
Issue #77 (mission log) names the delegation tree as a "mission" and keys its log entries by a mission identifier. Denormalizing that identifier onto the tables that already carry per-token and per-event rows is the cheap, obvious move that makes both CoSAI-style audit queries and #77's mission log reads O(1).
Rescoped from earlier drafts. Originally proposed as a new correlation_id UUID generated at issuance. Superseded: introducing a parallel workflow UUID would duplicate identity that can be derived from the existing delegation tree. Also pivoted away from an interim root_jti name in favor of the domain-aligned mission_id — it names what the field does (groups a workflow's events) rather than what it currently happens to be (a JTI).
Proposed change
Domain / schema
Add nullable mission_id VARCHAR(255) column to issued_credentials with an index.
Add nullable mission_id VARCHAR(255) column to cae_signals with an index.
Service layer (internal/service/oauth.go, credential.go)
First issuance (any grant that originates a credential — client_credentials, jwt_bearer, authorization_code, api_key, external_principal_exchange, refresh_token): mission_id = <this credential's own JTI>. The root of a delegation tree starts a new mission.
token_exchange: derive mission_id from the subject_token:
If the subject_token has an explicit mission_id claim, use it.
Otherwise (pre-migration or flat issuance), fall back to the subject_token's own jti — the subject is a mission root by definition.
Write the derived value to the new credential's mission_id column.
Embed mission_id as a claim on every issued JWT.
Logging
Where request context carries a verified token, the zerolog child logger automatically includes mission_id as a structured field. Applies to token endpoint, introspection, revocation, and credential lifecycle logs.
Admin API
GET /credentials?mission_id=<id> — returns all credentials in the delegation tree for the given mission, ordered by delegation_depth then created_at.
GET /signals?mission_id=<id> — same, for CAE signals.
authjwt client library
Add (*Claims).MissionID() string so downstream services can log and filter by it without parsing claims manually.
Claim name:mission_id. This is a ZeroID-private claim (not IANA-registered). Collision risk is accepted within the ZeroID ↔ authjwt closed ecosystem. If a standards body (IETF WIMSE, CoSAI) later registers an equivalent claim, ZeroID can alias the registered name onto the existing field — the opaque-value contract protects consumers from the transition.
Per-request trace IDs (OpenTelemetry style). Chi's request_id already covers single-request correlation; this issue is about workflow-spanning correlation.
Rich mission concepts (description, approval lifecycle, clarification dialogue) from the AAuth draft. mission_id groups events; it does not imply AAuth's richer mission semantics are implemented.
Compatibility notes
mission_id is nullable. Pre-migration credentials will not have it populated and will not be retrievable via the new filter; parent_jti chain walking still works for them. No backfill required — old credentials expire via TTL.
Additive to the JWT shape. No existing claim changed.
authjwt addition is a non-breaking method on Claims.
Opaque value contract. Consumers MUST NOT attempt to interpret mission_id (e.g., by looking up a credential by its JTI). This keeps the population scheme free to evolve.
Acceptance criteria
First-issuance grants write mission_id = <own jti> to the credential row and embed the mission_id claim in the JWT.
token_exchange correctly propagates mission_id from the subject_token to the new credential (both explicit-claim and fallback-to-subject-jti paths).
mission_id appears in zerolog output when the request carries a verified token.
Admin API supports ?mission_id=<id> on credentials and signals, returning ordered results.
Integration test: issue root credential → exchange twice to build a 3-node chain → GET /credentials?mission_id=<mid> returns all 3 in correct order.
Migration applies and reverts cleanly.
authjwt.Claims.MissionID() exposed and covered by a unit test.
Docs explicitly call out that mission_id is opaque to consumers.
Summary
Surface
mission_id— a stable, opaque identifier for a delegation tree — as a first-class, queryable field: a JWT claim, a column onissued_credentialsandcae_signals, a log field, and an admin API filter. Enables efficient workflow-level audit queries without walking theparent_jtichain at read time.Implementation note:
mission_idis currently populated with the JTI of the root credential in the delegation tree. Consumers MUST treat it as opaque — do not assume the value is a JTI, do not attempt to look up the underlying credential by this ID. Keeping the value opaque lets the population scheme evolve (e.g., to match a future standardized claim) without breaking consumers.Motivation
CoSAI Agentic IAM Appendix D's "prove control on demand" query list includes "Retrieve events sharing the same correlation_id, ordered by delegation depth" — reconstruct a full workflow end-to-end. ZeroID's existing
parent_jtilinks let you walk this chain, but every workflow query becomes an O(depth) recursive CTE.Issue #77 (mission log) names the delegation tree as a "mission" and keys its log entries by a mission identifier. Denormalizing that identifier onto the tables that already carry per-token and per-event rows is the cheap, obvious move that makes both CoSAI-style audit queries and #77's mission log reads O(1).
Rescoped from earlier drafts. Originally proposed as a new
correlation_idUUID generated at issuance. Superseded: introducing a parallel workflow UUID would duplicate identity that can be derived from the existing delegation tree. Also pivoted away from an interimroot_jtiname in favor of the domain-alignedmission_id— it names what the field does (groups a workflow's events) rather than what it currently happens to be (a JTI).Proposed change
Domain / schema
mission_id VARCHAR(255)column toissued_credentialswith an index.mission_id VARCHAR(255)column tocae_signalswith an index.009_mission_id_denormalization.up.sql, reversible.Service layer (
internal/service/oauth.go,credential.go)client_credentials,jwt_bearer,authorization_code,api_key,external_principal_exchange,refresh_token):mission_id = <this credential's own JTI>. The root of a delegation tree starts a new mission.token_exchange: derivemission_idfrom thesubject_token:mission_idclaim, use it.jti— the subject is a mission root by definition.mission_idcolumn.mission_idas a claim on every issued JWT.Logging
mission_idas a structured field. Applies to token endpoint, introspection, revocation, and credential lifecycle logs.Admin API
GET /credentials?mission_id=<id>— returns all credentials in the delegation tree for the given mission, ordered bydelegation_depththencreated_at.GET /signals?mission_id=<id>— same, for CAE signals.authjwtclient library(*Claims).MissionID() stringso downstream services can log and filter by it without parsing claims manually.Claim name:
mission_id. This is a ZeroID-private claim (not IANA-registered). Collision risk is accepted within the ZeroID ↔ authjwt closed ecosystem. If a standards body (IETF WIMSE, CoSAI) later registers an equivalent claim, ZeroID can alias the registered name onto the existing field — the opaque-value contract protects consumers from the transition.Out of scope
request_idalready covers single-request correlation; this issue is about workflow-spanning correlation.mission_idgroups events; it does not imply AAuth's richer mission semantics are implemented.Compatibility notes
mission_idis nullable. Pre-migration credentials will not have it populated and will not be retrievable via the new filter;parent_jtichain walking still works for them. No backfill required — old credentials expire via TTL.authjwtaddition is a non-breaking method onClaims.mission_id(e.g., by looking up a credential by its JTI). This keeps the population scheme free to evolve.Acceptance criteria
mission_id = <own jti>to the credential row and embed themission_idclaim in the JWT.token_exchangecorrectly propagatesmission_idfrom the subject_token to the new credential (both explicit-claim and fallback-to-subject-jti paths).mission_idappears in zerolog output when the request carries a verified token.?mission_id=<id>on credentials and signals, returning ordered results.GET /credentials?mission_id=<mid>returns all 3 in correct order.authjwt.Claims.MissionID()exposed and covered by a unit test.mission_idis opaque to consumers.Relationship to #77
mission_log.mission_idis the same value this issue writes toissued_credentials.mission_id. One concept, one name.GET /oauth2/mission/log?mission_id=...) works — but the corresponding "what tokens were issued during this mission" query has to walkparent_jtieach time. Shipping this first removes that penalty permanently.Non-goals
correlation_id,workflow_id, orroot_jtifield. The codebase has exactly one name for this identifier:mission_id.mission_idthrough unauthenticated request contexts (e.g., health checks). Only emit the log field when there is a verified token.mission_idis the identifier only.