Agent Identity and Permissions¶
Level: Advanced · Time: ~45 min · Exam relevance: CCAR-P D3 (3.2), CCAR-P D5 (5.1), CCAR-P D1 (1.4)
In plain English¶
Every action in a well-run system can be traced to an identity: a person or a service. Agents blur this. When an agent sends an email, who sent it? The user who asked, the agent, the developer who built it, or the service account it happens to run under? If you cannot answer, you cannot authorize correctly, audit afterwards, or revoke access when something goes wrong.
Agent identity means giving each agent (and each running instance where it matters) a non-human identity (NHI), and giving it permissions that are scoped to the task, short-lived, and derived from (never larger than) the rights of the person it is acting for. That last property is called delegation.
Why it matters¶
- OWASP's agentic list includes ASI03 Identity and Privilege Abuse; LLM06 Excessive Agency in the LLM list covers excessive permissions.
- CCAR-P-3.2 asks you to analyse authentication and authorisation for gaps. The most common gap in agent designs is a shared, long-lived, over-privileged service credential.
- Regulators and auditors expect you to show who did what. "The AI did it" is not an identity.
How it works¶
Three identities in every agent action¶
| Identity | Question it answers | Example |
|---|---|---|
| Principal (user) | On whose behalf? | alice@contoso-health.example |
| Agent (NHI) | Which software actor? | agent:benefits-assistant@prod |
| Credential | With which proof and scope? | Token, audience = HR API, scope = benefits:read, expires in 10 minutes, act = agent |
Record all three on every tool call. OAuth 2.0 Token Exchange (RFC 8693) expresses this directly: an exchanged token
carries the user as the subject and the agent in an act (actor) claim.
Patterns for credentials¶
| Pattern | How it works | Use when | Risk |
|---|---|---|---|
| Shared service account | One long-lived key for the agent, all users | Never for user data; maybe for public data | Confused deputy, no attribution, large blast radius |
| Per-agent workload identity | Agent has its own identity with fixed least-privilege scopes, short-lived tokens from the platform (cloud IAM roles, workload identity federation) | Back-office agents with no end user | Must still be scoped tightly; no per-user restriction |
| Delegated user token | Agent acts with a token derived from the user's session, down-scoped to the task | Assistants acting for a signed-in user | Token handling inside the agent runtime; keep out of model context |
| Brokered credentials | A broker or vault holds secrets and injects them at the network edge; the agent runtime never sees them | Agents that run untrusted code or read untrusted content | Broker becomes critical infrastructure |
Rules for delegation¶
- Never more than the user. Effective permission = intersection of the user's rights, the agent's allowed scopes and the task's needs.
- Short-lived. Minutes, not days. Refresh through the broker, not the model.
- Audience-bound. A token for the HR API must not work on the payroll API.
- Non-transitive by default. If an orchestrator delegates to a sub-agent, the sub-agent receives a further down-scoped token. An orchestrator cannot grant what it does not hold.
- Revocable. Kill switches per agent identity and per user grant.
- Never in the context window. Tokens are handled by your tool executor, not passed through the model, because anything in context can be leaked (see secrets management).
sequenceDiagram
participant U as Alice (signed in)
participant A as Agent runtime
participant B as Token broker
participant M as Claude
participant API as HR API
U->>A: "What is my remaining dental allowance?"
A->>M: messages + tools (no tokens)
M-->>A: tool_use get_benefit_balance(plan="dental")
A->>B: exchange(user=Alice session, agent=benefits-assistant, aud=HR API, scope=benefits:read)
B-->>A: token (10 min, sub=Alice, act=benefits-assistant)
A->>API: GET /benefits/dental (token)
API-->>A: balance for Alice only
A-->>M: tool_result
What Anthropic's platforms provide (verified 2026-10-24)¶
- Claude API authentication: besides API keys, the SDKs support OAuth profiles and Workload Identity Federation, where a workload exchanges an identity token from its own platform for short-lived Anthropic credentials tied to a service account. This removes long-lived API keys from agent hosts. See the Admin API for service accounts and federation rules.
- Workspaces: separate API keys, limits and access per workspace so one agent's key cannot spend another's budget (see Workspaces).
- Claude Code cloud sessions: Anthropic's security page states that the long-lived GitHub credential stays on Anthropic's side and is never copied into the session's virtual machine. Inside the VM there is only a temporary, session-limited credential, and a proxy adds the real credential to GitHub requests on the way out.
- Claude Managed Agents: credentials can be stored in vaults and substituted at egress, so they are not visible inside the agent's sandbox.
These are the brokered-credential pattern built into the platform. For your own agents, you implement the same pattern with your identity provider and a token broker.
Example¶
A minimal, standard-library token broker that demonstrates down-scoping and delegation chains. In production you would use your identity provider's token exchange endpoint; the logic of intersection, audience and expiry is the same.
"""delegation.py - illustrate down-scoped, short-lived, audience-bound delegated tokens."""
import base64
import hashlib
import hmac
import json
import os
import time
SIGNING_KEY = os.environ.get("BROKER_SIGNING_KEY", "").encode() # set in your environment, never hardcode
AGENT_ALLOWED_SCOPES = {
"benefits-assistant": {"benefits:read"},
"claims-subagent": {"claims:read"},
}
def _sign(payload: dict) -> str:
body = base64.urlsafe_b64encode(json.dumps(payload, sort_keys=True).encode()).decode()
mac = hmac.new(SIGNING_KEY, body.encode(), hashlib.sha256).hexdigest()
return f"{body}.{mac}"
def verify(token: str, audience: str) -> dict:
body, mac = token.rsplit(".", 1)
expected = hmac.new(SIGNING_KEY, body.encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(mac, expected):
raise PermissionError("bad signature")
claims = json.loads(base64.urlsafe_b64decode(body))
if claims["aud"] != audience or claims["exp"] < time.time():
raise PermissionError("wrong audience or expired")
return claims
def exchange(user: str, user_scopes: set[str], agent: str, audience: str,
requested: set[str], parent: dict | None = None, ttl: int = 600) -> str:
"""Effective scopes = user ∩ agent policy ∩ request (∩ parent token if delegating further)."""
if not SIGNING_KEY:
raise RuntimeError("BROKER_SIGNING_KEY is not set")
effective = user_scopes & AGENT_ALLOWED_SCOPES.get(agent, set()) & requested
chain = [agent]
if parent is not None:
effective &= set(parent["scope"].split())
chain = parent["act_chain"] + [agent]
if not effective:
raise PermissionError("no permitted scopes for this request")
return _sign({"sub": user, "act_chain": chain, "aud": audience,
"scope": " ".join(sorted(effective)), "exp": int(time.time()) + ttl})
if __name__ == "__main__":
os.environ.setdefault("BROKER_SIGNING_KEY", "demo-only-not-a-secret")
SIGNING_KEY = os.environ["BROKER_SIGNING_KEY"].encode()
t = exchange("alice", {"benefits:read", "claims:read", "payroll:read"},
"benefits-assistant", "https://hr.contoso-health.example",
requested={"benefits:read", "payroll:read"})
print(verify(t, "https://hr.contoso-health.example")) # scope: benefits:read only
Notice that payroll:read is requested and the user holds it, but the agent's policy does not allow it, so the token
does not carry it. A sub-agent token created with parent= can only shrink further.
Enterprise use case¶
Contoso Health runs three agents: a benefits assistant for employees, a claims-review agent for the claims team and a nightly reconciliation agent with no end user. The identity design:
| Agent | Identity | Credential pattern | Notes |
|---|---|---|---|
| Benefits assistant | agent:benefits-assistant |
Delegated user token, benefits:read, 10 min |
Can only see the requesting employee's data |
| Claims-review agent | agent:claims-review |
Delegated reviewer token; writes require approval | Sub-agents get read-only tokens |
| Reconciliation agent | agent:recon-nightly |
Workload identity, fixed scopes, no user | Runs in an isolated account; outputs a report, writes nothing |
All three call the Claude API through workload identity federation instead of long-lived API keys, each in its own
workspace with its own spend limit. Audit logs record sub, act_chain, tool, outcome and request ID.
Common mistakes¶
- One API key or service account shared by every agent and environment.
- Passing OAuth tokens to the model in the prompt so it can "call the API itself".
- Long-lived refresh tokens stored in agent memory or logs.
- Sub-agents inheriting the orchestrator's full credential.
- No way to revoke a single agent's access without breaking all of them.
Security considerations¶
- Inventory NHIs like human accounts: owner, purpose, scopes, last used, expiry. Remove stale ones.
- Alert on unusual NHI behaviour: new tools, volume spikes, access outside working patterns.
- Separate environments: development agents must not hold production credentials.
- Human approvals should be recorded against the approver's identity as well as the agent's.
Troubleshooting¶
| Symptom | Likely cause | Fix |
|---|---|---|
| Agent works for admins but fails for regular users | Delegation now enforces real user rights | This is correct; adjust the UX to explain the limit |
| Audit logs show only the service account | No actor claim or user context propagated | Use token exchange with actor claims; log both identities |
| Token expired mid-task | Long-running agent with short tokens | Refresh through the broker per tool call, not per task |
Check your understanding¶
- What three identities should be recorded for every agent tool call?
- How do you compute the effective scope of a delegated token?
- Why should tokens never pass through the model's context window?
- What does "non-transitive delegation" mean for a sub-agent?
Answers
- The principal (user), the agent (non-human identity) and the credential (with audience, scope and expiry).
- The intersection of the user's rights, the agent's allowed scopes, the task's requested scopes, and (for further delegation) the parent token's scopes.
- Anything in context can be leaked through outputs, logs or injected instructions; the tool executor should attach credentials outside the model.
- A sub-agent cannot receive more than its parent holds, and does not automatically receive everything the parent holds; it gets a further down-scoped token.
Further reading¶
- Anthropic: Admin API and Workspaces
- Anthropic: Claude Code security
- IETF: RFC 8693 OAuth 2.0 Token Exchange
- OWASP: Top 10 for Agentic Applications 2026
- In this repository: multi-agent architecture, managed agents and the Agent SDK, Lab 20