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
- Application Manager is running and the agent-identity EF migration has been applied (
AgentIdentities, AgentPublicKeys, AgentCredentials tables present).
- The target
AgentIdentity exists, is active (IsActiveAt(now) == true), and is not expired/disabled/revoked (UC-025).
- The agent has at least one valid, non-revoked
AgentPublicKey whose validity window covers the current time.
- The requested scopes are within the agent's grantable scope ceiling and the requested TTL is within the agent's ceiling max-TTL.
- 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.
- (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
- An
AgentCredential row is persisted with effective scopes, IssuedAt, ExpiresAt, encrypted token reference, and (for OBO) OnBehalfOfSubjectId.
- A short-TTL JWT (RFC 9068) is returned to the agent, DPoP-bound to the proving key when DPoP is used.
- The credential's effective scopes are the intersection of the request and the agent's scope ceiling — never a superset.
- Introspection of the credential returns
active: true with the correct scopes/expiry until expiry or revocation.
- A mint/exchange/introspect/revoke event is written to
AuditLog with the agent id and acting principal.
Failure Postconditions
- 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.
- A structured RFC 7807 Problem Details (or OAuth
error) response is returned; no token is issued.
- A revoked or expired credential introspects as
active: false; downstream calls using it are rejected.
- 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
- The caller submits the agent id, a requested scope set, and a requested TTL (seconds) via
IssueAgentCredential.
- The handler loads the
AgentIdentity and confirms it is active (IsActiveAt(now)), not expired/disabled/revoked.
- The handler resolves the agent's grantable scope ceiling and ceiling max-TTL.
- 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).
- The handler clamps the TTL to the ceiling max-TTL; a request above the ceiling is rejected (A1).
- The handler generates the credential token, encrypts the token material via
AgentCredentialTokenProtector (purpose AgentCredentials), and persists an AgentCredential row (effective scopes, IssuedAt, ExpiresAt).
- The handler writes an
IssueAgentCredential event to AuditLog.
- 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
- The agent constructs a JWT client assertion signed with its private key, with the
kid header naming a registered AgentPublicKey.
- The agent constructs a DPoP proof JWT bound to the request method/URI (
htm, htu), a unique jti, and iat.
- The agent POSTs both to
/api/agents/token along with the requested scope and (optionally) a requested TTL.
- 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).
- AM verifies the DPoP proof: signature,
htu/htm match, iat freshness, and jti replay protection.
- AM enforces governance: requested scope ⊆ ceiling, requested TTL ≤ max-TTL (UC-026a rules).
- AM mints an RFC 9068 JWT carrying the effective scopes and a
cnf (confirmation) claim binding the token to the DPoP key (jkt thumbprint).
- AM persists the
AgentCredential (encrypted token ref) and writes the audit event.
- 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
- The verifier POSTs the token to
/api/agents/introspect with its X-Api-Key.
- The
IntrospectCredential query resolves the AgentCredential from the token reference.
- AM evaluates liveness: not revoked,
ExpiresAt > now, owning agent still active.
- 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).
- 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
- The caller submits the agent id and credential id via
RevokeAgentCredential.
- The handler loads the
AgentCredential, sets RevokedAt = now, and persists.
- The handler writes a
RevokeAgentCredential audit event with the acting principal.
- 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
- Because tokens are short-TTL JWTs (RFC 9068), an expired token fails standard
exp validation at the resource server without a round-trip.
- Introspection of an expired credential returns
active: false (UC-026c, A2).
- 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.
- 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 |