OAuth for agent delegation
How RFC 9396 Rich Authorization Requests, RFC 8693 token exchange, DPoP, CIBA and the IETF agent-delegation drafts shape PermDock's two-principal subject and the rule that an agent can never exceed its user.
Draft posture: build (RFC 8693, RFC 9396, RFC 9449; remaining IETF agent-delegation drafts tracked on the watch list)
PermDock implements the two-principal subject (principal, actor, delegation), matches RAR authorization_details against permissions, records RFC 8693 act chains, and derives actor from Web Bot Auth signatures.
What it is
A cluster of OAuth specifications and drafts describes how a human's authority is handed to software that acts for them:
- RFC 9396 Rich Authorization Requests (RAR). Replaces flat scopes with an
authorization_detailsarray of typed JSON objects (type,actions,locations,datatypes, and type-specific fields). It is the carrier of constrained authority: not "may access posts" but "may update posts where the author is this user". - RFC 8693 token exchange. Lets a client trade one token for another, optionally with an
actor_token, producing tokens that name both who the request is for and who is making it (actclaim). - DPoP (RFC 9449). Binds a token to a key so a stolen token is useless without the private key; relevant when agents hold long-lived credentials.
- CIBA. Client-Initiated Backchannel Authentication: the agent asks, the user approves on a separate device. A standards-based human-in-the-loop primitive.
- Agent Delegation Chain (WIMSE draft): carries authority as RAR
authorization_detailsacross multiple agent hops and enforces offline-verifiable monotonic attenuation: each hop can only narrow what it received, and a verifier can check the whole chain without calling back to the issuer. - Credential Delegation Protocol (WIMSE draft): composes RFC 8693, DPoP, RAR and CIBA into one delegation flow.
- OAuth for AI agents on behalf of a user: adds
requested_actorto the authorization request andactor_tokento the token request so the resulting token names the agent as well as the user. - MCP Enterprise-Managed Authorization. ID-JAG via RFC 8693 token exchange redeemed with RFC 7523; see MCP authorization.
- Transaction tokens. Short-lived tokens that carry authorization context across microservices inside a trust domain.
Why it matters for PermDock
The net effect of these specifications is that an authorization check is not "can this user" but "can this user, acting through this agent, under this delegated authority". A single-subject API cannot express that, so PermDock's subject has two principals:
principal: the human or service whose grants are evaluated.actor(optional): the agent making the call: an MCPclientId, an AI SDK agent name, a Web Bot Auth key, an A2A card.delegation(optional): the authority the principal handed to the actor: OAuth scopes and/or RARauthorization_details, or an attenuated delegation chain.
A decision is principal grants intersected with delegated authority. The intersection is what makes "an agent can never exceed its user" a structural property rather than a policy the developer has to remember to write. See subject and delegation.
How PermDock uses it
import { createPermDock } from "permdock";
// Human case
const permdock = await createPermDock(policy, user);
// Agent case: actor + delegation
const asAgent = await createPermDock(policy, user, {
actor: { id: authInfo.clientId, kind: "mcp-client" },
delegation: {
scopes: authInfo.scopes, // OAuth scopes
authorizationDetails: authInfo.authorization_details, // RFC 9396 objects, if present
},
});
asAgent.can(permissions.post.update, post); // true only if the user may AND the delegation covers itAdapters fill actor and delegation automatically from authInfo (MCP), runtimeContext (AI SDK), the verified signature (Web Bot Auth) or the Agent Card (A2A), so application code rarely constructs them by hand.
Matching authorization_details to permissions
Each permission reference carries a scope (post:update), and RAR entries are matched on the permission's resource and action. An entry in delegation.authorizationDetails covers a permission when its type equals the resource name and its actions, when present, include the permission's action. An identifier, when present, narrows the entry to the one resource whose id it names; an empty actions array covers nothing. Other members (locations, datatypes, type-specific constraints) are the authorization server's to render and enforce; PermDock does not widen or narrow a grant from them, and the principal's own grant conditions still apply.
{
"type": "post",
"actions": ["update"],
"locations": ["https://api.example.com/posts"]
}Delegation chains
Chain verification belongs to the token layer. permdock/jwt checks that a nested RFC 8693 act claim is well formed, takes the outermost act.sub as the actor (RFC 8693 section 4.1: the current actor; nested act claims are prior actors and never decide access), and records the whole chain as delegation.chain for audit; a malformed chain yields the anonymous subject. Core trusts the scopes and authorization_details the verified token carries.
Calls with no human principal
A service agent acting on its own behalf is a principal with kind: 'service' and its own roles, not a separate subject shape. Between services inside one trust domain, an AuthZEN call carries the decision; PermDock defines no transaction token profile.
Attenuation invariants
- A delegation may only remove or narrow; a grant that the principal lacks cannot be added by any
authorization_detailsentry,actor_tokenor chain hop. - An empty
scopeslist is no delegated authority: every check isdeniedwith reasonno-delegation. Anactorwith nodelegationat all is denied the same way; a subject with noactoris a human call and is not narrowed by delegation.
What PermDock does not do
PermDock does not issue tokens, run token exchange, validate DPoP proofs or drive CIBA. Those belong to the authorization server and the transport layer. PermDock consumes their output.
Mapping table
| OAuth concept | PermDock concept |
|---|---|
| Resource owner / user in the token | principal |
client_id, act claim, actor_token, requested_actor | actor |
OAuth scope | delegation.scopes, matched against permission.scope |
RFC 9396 authorization_details | delegation.authorizationDetails; type matched against the resource name, actions against the action |
| RAR constraint payload | Enforced by the authorization server; the grant's own condition still applies |
Agent Delegation Chain hops, RFC 8693 act | delegation.chain, verified by the token layer and recorded for audit |
| Monotonic attenuation | Structural: decision = principal grants ∩ delegation |
| RFC 8693 token exchange, ID-JAG | Transparent; resulting token's claims feed principal, actor, delegation |
| DPoP, CIBA | Transport and consent layer; CIBA is one way approval-required can be fulfilled |
| Transaction tokens | Not used; services call each other's AuthZEN endpoint |
| Service agent with no human | principal with kind: 'service' |
Sources
- Agent Delegation Chain draft.
- Credential Delegation Protocol draft.
- OAuth for AI agents on behalf of a user draft.
- MCP Enterprise-Managed Authorization.
- RFC 9396, RFC 8693, RFC 9449 and RFC 7523 are referenced by number; see the drafts above for how they compose.
- two-principal subject.
Last updated on
OpenID AuthZEN
How PermDock speaks the OpenID AuthZEN Authorization API 1.0 as a policy decision point (permdock/authzen) and as a policy enforcement point (the pdp provider).
Shared Signals and CAEP
How the OpenID Shared Signals Framework 1.0 and CAEP 1.0 let permdock/ssf invalidate snapshots the moment an identity provider revokes a session instead of waiting for a TTL.