Use Case 026: Agent Credential Issuance

Overview

Property Value
Use Case ID UC-026
Use Case Name Agent Credential Issuance
Module Agent Identity — Identity Backend
Priority Critical
Status ✅ Implemented
Version 1.0
Last Updated June 15, 2026

Implementation status (agent-identity release, June 2026). Implemented: ephemeral scoped credential issuance, OAuth 2.1 private_key_jwt client authentication (RFC 7523, ES256) at POST /api/agents/token, RFC 7662-style introspection at POST /api/agents/introspect, public-key registration, and revocation — with one-time-use (jti) replay protection (AgentClientAssertionVerifier, ConsumedAgentAssertions). Credential secrets are stored encrypted (Data Protection purpose AgentCredentials). Gap: DPoP is reserved but not yet enforced — AgentTokenRequest.DpopProof is accepted and ignored. Canonical types: AgentCredential, AgentPublicKey.

Description

This use case describes how Application Manager mints ephemeral, scoped credentials for a registered agent identity (UC-025) and issues OAuth 2.1 access tokens that the agent presents when calling protected resources and the LLM gateway. The agent authenticates without any stored secret: it proves possession of a private key registered as an AgentPublicKey (UC-025) via private_key_jwt (RFC 7523) and binds each token to a sender key via DPoP (RFC 9449). Tokens are short-TTL JWTs (RFC 9068) carrying the credential's effective scopes; callers verify a credential's liveness through RFC 7662 introspection, and any credential can be revoked immediately. Every mint, exchange, introspection, and revocation is governed at issue time and recorded on the actor-chain audit pipeline.

This is part of the agent-identity release. The SDK master design is riptide-sdk/docs/plans/AGENT-IDENTITY-ARCHITECTURE.md; the Application Manager-side plan is docs/internal/agent-identity-plan.md (Section 1, plus §9 Reuse and §10 Resolved decisions, "Identity"). The credential token is encrypted at rest by AgentCredentialTokenProtector, an IDataProtector with the purpose string AgentCredentials — the same pattern as SyntheticPrincipalTokenProtector, with a distinct purpose string so a scoped key compromise never crosses secret classes. Governance is non-negotiable: a minted credential's TTL may never exceed the agent's ceiling max-TTL, and its requested scopes must be a subset of the agent's grantable scope ceiling.

Actors

Actor Description Role
AI Agent Non-human identity requesting credentials/tokens; holds the private key matching a registered AgentPublicKey Primary
Owner / Admin Human owner who mints credentials from the admin UI and can revoke them; holds agents.write Primary
Application Manager API X-Api-Key-authenticated REST API (AgentsController) hosting mint/token/introspect/revoke endpoints Supporting
Application Manager Web Bootstrap 5 + Tabler MVC admin UI for credential listing and revocation Supporting
AgentCredentialTokenProtector IDataProtector (purpose AgentCredentials) encrypting token material at rest Supporting
Scope-ceiling resolver Resolves the agent's effective scope ceiling and max-TTL at issue time (reuses the ISyntheticPrincipalResolver shape) Supporting
AuditLog Append-only audit trail recording mint / exchange / introspect / revoke with actor chain Supporting
Resource server / LLM gateway Downstream verifier that accepts the agent token and DPoP proof, optionally calling introspection External

Preconditions

  1. Application Manager is running and the agent-identity EF migration has been applied (AgentIdentities, AgentPublicKeys, AgentCredentials tables present).
  2. The target AgentIdentity exists, is active (IsActiveAt(now) == true), and is not expired/disabled/revoked (UC-025).
  3. The agent has at least one valid, non-revoked AgentPublicKey whose validity window covers the current time.
  4. The requested scopes are within the agent's grantable scope ceiling and the requested TTL is within the agent's ceiling max-TTL.
  5. The caller minting via API presents a valid X-Api-Key; the agent authenticating to /api/agents/token holds the private key corresponding to a registered kid.
  6. (For gateway use) the agent has viable budget headroom under an active AgentPlan or PerTurnBudget default (UC-032/UC-033) — enforced downstream by the gateway, not at mint time.

Postconditions

Success Postconditions

  1. An AgentCredential row is persisted with effective scopes, IssuedAt, ExpiresAt, encrypted token reference, and (for OBO) OnBehalfOfSubjectId.
  2. A short-TTL JWT (RFC 9068) is returned to the agent, DPoP-bound to the proving key when DPoP is used.
  3. The credential's effective scopes are the intersection of the request and the agent's scope ceiling — never a superset.
  4. Introspection of the credential returns active: true with the correct scopes/expiry until expiry or revocation.
  5. A mint/exchange/introspect/revoke event is written to AuditLog with the agent id and acting principal.

Failure Postconditions

  1. No AgentCredential row is created when the agent is inactive, the key is invalid, the scope exceeds the ceiling, or the TTL exceeds the ceiling.
  2. A structured RFC 7807 Problem Details (or OAuth error) response is returned; no token is issued.
  3. A revoked or expired credential introspects as active: false; downstream calls using it are rejected.
  4. Failed mint/auth attempts are recorded in AuditLog (without leaking key material).

Primary Flow Sequence

sequenceDiagram
    participant Agent as AI Agent
    participant API as AM API (AgentsController)
    participant Resolver as Scope-Ceiling Resolver
    participant Protector as AgentCredentialTokenProtector
    participant Repo as IAgentCredentialRepository
    participant Audit as AuditLog

    Agent->>API: POST /api/agents/token (private_key_jwt assertion + DPoP proof, scope, ttl)
    API->>Resolver: Resolve agent, verify assertion against AgentPublicKey (kid)
    Resolver-->>API: Agent active, public key valid, scope ceiling + max-TTL
    API->>API: Validate DPoP proof (htu, htm, jti, iat)
    API->>API: Check requested scope ⊆ ceiling AND ttl ≤ max-TTL
    alt scope or TTL exceeds ceiling
        API-->>Agent: 400 invalid_scope / invalid_request (Problem Details)
        API->>Audit: Record denied mint (agent id, reason)
    else within ceiling
        API->>API: Mint RFC 9068 JWT (effective scopes, DPoP cnf, exp)
        API->>Protector: Protect("AgentCredentials", token material)
        Protector-->>API: Encrypted token reference
        API->>Repo: Persist AgentCredential (scopes, IssuedAt, ExpiresAt)
        Repo-->>API: credentialId
        API->>Audit: Record IssueAgentCredential (agent id, cid, scopes)
        API-->>Agent: 200 { access_token, token_type: DPoP, expires_in, scope }
    end

UC-026a: Mint Ephemeral Scoped Credential

Triggers

  • An owner/admin mints a credential from the admin UI, or an automation calls POST /api/agents/{id}/credentials with a requested scope subset and TTL.

Basic Flow

  1. The caller submits the agent id, a requested scope set, and a requested TTL (seconds) via IssueAgentCredential.
  2. The handler loads the AgentIdentity and confirms it is active (IsActiveAt(now)), not expired/disabled/revoked.
  3. The handler resolves the agent's grantable scope ceiling and ceiling max-TTL.
  4. The handler computes the effective scope set as the intersection of the requested scopes and the ceiling; if the request named scopes outside the ceiling, they are dropped (or the request rejected — see A2).
  5. The handler clamps the TTL to the ceiling max-TTL; a request above the ceiling is rejected (A1).
  6. The handler generates the credential token, encrypts the token material via AgentCredentialTokenProtector (purpose AgentCredentials), and persists an AgentCredential row (effective scopes, IssuedAt, ExpiresAt).
  7. The handler writes an IssueAgentCredential event to AuditLog.
  8. The credential reference and effective scopes are returned. (The raw token is shown once; only the encrypted reference is stored.)

Alternative Flows

  • A1: TTL exceeds ceiling — Requested TTL > agent ceiling max-TTL; the command is rejected with invalid_request; denial audited.
  • A2: Scope exceeds ceiling (strict mode) — A requested scope is outside the ceiling; reject with invalid_scope rather than silently dropping; denial audited.
  • A3: Agent inactive — Agent expired/disabled/revoked; reject with agent_inactive; no credential created.
  • A4: No grantable scopes — Intersection is empty; reject with invalid_scope.

UC-026b: OAuth 2.1 Token Exchange (private_key_jwt + DPoP)

Triggers

  • An agent calls POST /api/agents/token to obtain an access token, presenting a private_key_jwt client assertion (RFC 7523) and a DPoP proof (RFC 9449).

Basic Flow

  1. The agent constructs a JWT client assertion signed with its private key, with the kid header naming a registered AgentPublicKey.
  2. The agent constructs a DPoP proof JWT bound to the request method/URI (htm, htu), a unique jti, and iat.
  3. The agent POSTs both to /api/agents/token along with the requested scope and (optionally) a requested TTL.
  4. AM resolves the agent and the named kid, confirms the key's validity window covers now and the key is not revoked, and verifies the assertion signature (ECDSA P-256).
  5. AM verifies the DPoP proof: signature, htu/htm match, iat freshness, and jti replay protection.
  6. AM enforces governance: requested scope ⊆ ceiling, requested TTL ≤ max-TTL (UC-026a rules).
  7. AM mints an RFC 9068 JWT carrying the effective scopes and a cnf (confirmation) claim binding the token to the DPoP key (jkt thumbprint).
  8. AM persists the AgentCredential (encrypted token ref) and writes the audit event.
  9. AM returns { access_token, token_type: "DPoP", expires_in, scope }.
sequenceDiagram
    participant Agent as AI Agent
    participant API as AM API (token endpoint)
    participant Keys as IAgentPublicKeyRepository
    participant DPoP as DPoP Verifier
    participant Audit as AuditLog

    Agent->>API: POST /api/agents/token (client_assertion=private_key_jwt, DPoP header, scope)
    API->>Keys: Lookup AgentPublicKey by kid
    Keys-->>API: ECDSA P-256 public key (valid window, not revoked)
    API->>API: Verify client_assertion signature (RFC 7523)
    API->>DPoP: Verify DPoP proof (htu, htm, jti freshness, iat)
    DPoP-->>API: Proof valid, jkt thumbprint
    API->>API: scope ⊆ ceiling? ttl ≤ max-TTL?
    alt governance fails
        API-->>Agent: 400 invalid_scope / invalid_request
        API->>Audit: Record denied token request
    else governance ok
        API->>API: Mint RFC 9068 JWT (scopes, cnf.jkt, exp)
        API->>Audit: Record IssueAgentCredential (DPoP-bound)
        API-->>Agent: 200 { access_token, token_type: DPoP, expires_in }
    end

Alternative Flows

  • A1: Unknown or revoked kid — No matching valid AgentPublicKey; reject with invalid_client; audited.
  • A2: Assertion signature invalid — Signature does not verify against the public key; reject with invalid_client.
  • A3: DPoP proof replay — jti already seen within the freshness window; reject with invalid_dpop_proof.
  • A4: DPoP htu/htm mismatch — Proof not bound to this request; reject with invalid_dpop_proof.
  • A5: Clock skew — iat outside the allowed window; reject with invalid_dpop_proof.

UC-026c: Introspect Credential

Triggers

  • A resource server or the LLM gateway calls POST /api/agents/introspect (RFC 7662) to confirm a presented credential is still live before honoring it.

Basic Flow

  1. The verifier POSTs the token to /api/agents/introspect with its X-Api-Key.
  2. The IntrospectCredential query resolves the AgentCredential from the token reference.
  3. AM evaluates liveness: not revoked, ExpiresAt > now, owning agent still active.
  4. AM returns the RFC 7662 response: active, scope, sub (agent id), exp, iat, and cnf.jkt for DPoP-bound tokens; on_behalf_of is included for delegated credentials (UC-027).
  5. The introspection is recorded (lightweight; not the noisy heartbeat class).

Alternative Flows

  • A1: Revoked credential — Returns { active: false }.
  • A2: Expired credential — ExpiresAt ≤ now; returns { active: false }.
  • A3: Owning agent disabled/revoked — Even if the credential itself has not expired, an inactive owner yields { active: false }.
  • A4: Unknown token — Returns { active: false } (no detail leak).

UC-026d: Revoke Credential

Triggers

  • An owner/admin revokes a credential via the admin UI, or an automation calls DELETE /api/agents/{id}/credentials/{cid}; revoking the owning agent (UC-025) cascades.

Basic Flow

  1. The caller submits the agent id and credential id via RevokeAgentCredential.
  2. The handler loads the AgentCredential, sets RevokedAt = now, and persists.
  3. The handler writes a RevokeAgentCredential audit event with the acting principal.
  4. Subsequent introspection of the credential returns active: false; the gateway and resource servers reject it.

Alternative Flows

  • A1: Already revoked — Idempotent; RevokedAt unchanged; no error.
  • A2: Credential not found — RFC 7807 404; no change.
  • A3: Agent revoked (cascade) — Revoking the owning agent marks all its outstanding credentials inactive at introspection time.

UC-026e: Credential Expiry / Automatic Invalidation

Triggers

  • A credential's ExpiresAt passes; no explicit action is required.

Basic Flow

  1. Because tokens are short-TTL JWTs (RFC 9068), an expired token fails standard exp validation at the resource server without a round-trip.
  2. Introspection of an expired credential returns active: false (UC-026c, A2).
  3. The agent re-mints (UC-026a) or re-exchanges (UC-026b) to obtain a fresh credential; ceiling and TTL governance re-apply on every issue.
  4. Expired AgentCredential rows remain for audit/forensics; an optional Hangfire sweep may archive rows past a retention window.

Alternative Flows

  • A1: Clock skew at the resource server — A small leeway is permitted per RFC 9068 validation; beyond leeway the token is rejected.
  • A2: Re-mint after agent ceiling tightened — If the agent's scope ceiling was narrowed (UC-025c) since the prior issue, the new credential reflects the tightened ceiling.

API Endpoints

Method Path Auth Purpose
POST /api/agents/{id}/credentials X-Api-Key Mint an ephemeral scoped credential (UC-026a)
POST /api/agents/token private_key_jwt (RFC 7523) + DPoP (RFC 9449) OAuth 2.1 token issuance (UC-026b)
POST /api/agents/introspect X-Api-Key RFC 7662 credential introspection (UC-026c)
DELETE /api/agents/{id}/credentials/{cid} X-Api-Key Revoke a credential (UC-026d)

Representative example — POST /api/agents/token

Request:

{
  "request": {
    "method": "POST",
    "path": "/api/agents/token",
    "headers": {
      "DPoP": "eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6IkVDIn19...",
      "Content-Type": "application/x-www-form-urlencoded"
    }
  },
  "body": {
    "grant_type": "client_credentials",
    "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
    "client_assertion": "eyJhbGciOiJFUzI1NiIsImtpZCI6ImtleS0yMDI2LTA2In0...",
    "scope": "billing:read mcp:invoice-server:get_invoice",
    "requested_ttl_seconds": 300
  }
}

Response (200 OK):

{
  "access_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCJ9...",
  "token_type": "DPoP",
  "expires_in": 300,
  "scope": "billing:read mcp:invoice-server:get_invoice",
  "credential_id": "01971f2a-6c3e-7b44-9f10-2a8d3c5e7b91",
  "agent_id": "01971f1a-2b3c-7d4e-8f90-1a2b3c4d5e6f",
  "cnf": { "jkt": "0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I" }
}

Business Rules

Rule ID Description
BR-1 A credential's TTL must be ≤ the owning agent's ceiling max-TTL; requests above the ceiling are rejected.
BR-2 A credential's effective scopes must be a subset of the agent's grantable scope ceiling — never a superset.
BR-3 No agent secret is ever stored; agents authenticate via private_key_jwt against a registered AgentPublicKey only.
BR-4 Token material at rest is encrypted via AgentCredentialTokenProtector (purpose AgentCredentials), distinct from ProviderAccountCredentials.
BR-5 Tokens are short-TTL JWTs (RFC 9068) and are DPoP-bound (cnf.jkt) when issued via the token endpoint.
BR-6 A DPoP proof's jti must not have been seen within the freshness window (replay protection).
BR-7 Revocation is immediate: a revoked credential introspects as active: false and is rejected downstream.
BR-8 Revoking the owning agent invalidates all its outstanding credentials at introspection time (cascade).
BR-9 Scopes are two-axis: coarse {app}:{action} capabilities and fine-grained tool scopes mcp:{server}:{tool}; both are bounded by the ceiling.
BR-10 Every mint, token issue, introspection, and revocation is written to AuditLog; failed attempts are audited without leaking key material.

Data Requirements

AgentCredential

Field Type Constraints
Id Guid (UUIDv7) Primary key
AgentId Guid FK → AgentIdentity, required, indexed
EncryptedTokenRef string Required; protected via AgentCredentialTokenProtector (purpose AgentCredentials)
EffectiveScopes string (space/JSON) Subset of agent scope ceiling; required
IssuedAt DateTimeOffset Set on mint, required
ExpiresAt DateTimeOffset Required; ≤ IssuedAt + agent ceiling max-TTL
RevokedAt DateTimeOffset? Null until revoked; non-null ⇒ inactive
OnBehalfOfSubjectId Guid? Null unless OBO (UC-027); subject user/agent id
DpopJkt string? DPoP key thumbprint (cnf.jkt) for sender-constrained tokens
Kid string? kid of the AgentPublicKey used for issuance
CreatedBy string? Acting principal (admin id or system)

Security Considerations

  • Authentication: Agents authenticate with private_key_jwt (RFC 7523) verified against a registered ECDSA P-256 AgentPublicKey; no shared secret exists. The /api/agents/credentials, /introspect, and revoke endpoints require a valid X-Api-Key.
  • Authorization / capabilities: Minting and revoking via the admin surface require agents.write; introspection is for trusted resource servers holding an API key. Effective scopes are intersected with the ceiling at issue time (no escalation).
  • Sender constraint: DPoP (RFC 9449) binds each token to a proof-of-possession key via cnf.jkt; a stolen bearer token cannot be replayed without the private key. jti replay protection and htu/htm/iat checks defend against proof reuse.
  • Data protection: Token material is encrypted at rest by AgentCredentialTokenProtector with the AgentCredentials purpose string; the raw token is surfaced once and never persisted in clear text.
  • Audit: Mint, token issue, introspect, and revoke events are written to AuditLog with agent id and acting principal; failures are audited without logging private-key or token material.

Testing Scenarios

Test ID Scenario Expected Result
T-1 Mint credential with scopes ⊆ ceiling, TTL ≤ max AgentCredential created; effective scopes = request; audit row written
T-2 Mint with TTL > ceiling max-TTL Rejected invalid_request; no credential; denial audited (BR-1)
T-3 Mint with a scope outside the ceiling (strict) Rejected invalid_scope; no credential (BR-2)
T-4 Mint against an inactive/disabled/revoked agent Rejected agent_inactive; no credential (UC-026a A3)
T-5 Token exchange with valid private_key_jwt + DPoP 200 RFC 9068 token, token_type: DPoP, cnf.jkt present
T-6 Token exchange with unknown/revoked kid invalid_client; no token; audited (UC-026b A1)
T-7 Token exchange with bad assertion signature invalid_client; no token (UC-026b A2)
T-8 Token exchange with replayed DPoP jti invalid_dpop_proof; no token (BR-6)
T-9 Token exchange with DPoP htu/htm mismatch invalid_dpop_proof; no token
T-10 Token exchange with stale iat (clock skew beyond leeway) invalid_dpop_proof; no token
T-11 Introspect a live, unexpired credential { active: true } with scopes, exp, cnf.jkt
T-12 Introspect a revoked credential { active: false } (BR-7)
T-13 Introspect an expired credential { active: false } (UC-026e)
T-14 Introspect a credential whose owning agent was revoked { active: false } (BR-8)
T-15 Introspect an unknown token { active: false }; no detail leak
T-16 Revoke a credential, then introspect RevokedAt set; introspection active: false
T-17 Revoke an already-revoked credential Idempotent; no error; RevokedAt unchanged
T-18 Revoke a non-existent credential RFC 7807 404; no change
T-19 Revoke owning agent, then use a previously minted credential Credential introspects active: false (cascade)
T-20 Verify token material is encrypted at rest Stored EncryptedTokenRef is ciphertext under AgentCredentials purpose; not clear text (BR-4)
T-21 Mint a credential requesting a fine-grained mcp:{server}:{tool} scope within ceiling Effective scopes include the tool scope (BR-9)
T-22 Re-mint after agent ceiling tightened New credential reflects tightened ceiling (UC-026e A2)
T-23 Use an expired short-TTL token at the resource server Rejected at exp validation without introspection round-trip
T-24 Mint endpoint called without a valid X-Api-Key 401; no credential
T-25 Empty scope intersection (no grantable scopes) Rejected invalid_scope (UC-026a A4)
  • UC-025: Agent Identity Registration & Lifecycle — provides the agent, its scope ceiling, max-TTL, and the AgentPublicKey records this UC verifies against.
  • UC-027: On-Behalf-Of Delegation — uses minted credentials and the token-exchange path to carry delegated scopes and budget.
  • UC-032: Cost-Governance Budget Headroom — viable-headroom checks the gateway applies when a credential is used.
  • UC-033: Agent Plan & Budget Delegation — plan context the gateway enforces alongside credential scopes.
  • UC-006: Role-Based Access Control — the Capability/Role model gating the admin mint/revoke surface.
  • UC-010: Activity Logging & Audit Trail — the AuditLog surface mint/exchange/revoke events fold into.

Revision History

Version Date Author Changes
1.0 June 9, 2026 Platform Architecture Team Initial draft