When a human employee takes an action on a company's systems, their identity is clear. Their user account is authenticated, their actions are attributed to them, and if something goes wrong, the audit log shows exactly who did what. When an AI agent takes an action on the same systems, the identity question becomes significantly more complex. Is the agent acting as itself? Is it acting as the user who triggered it? Is it acting as a service? These aren't philosophical questions. They're practical security architecture decisions with real consequences.
Getting agent identity wrong is one of the most common and consequential security mistakes in production agentic systems. This post covers the right mental model for agent identity, the authentication patterns that implement it correctly, and the audit infrastructure that makes agent actions attributable.
The Identity Problem in Agentic Systems
The core tension in agent identity is between capability and attribution. An agent needs enough permissions to do its job. But every permission it has is a permission that a compromised agent can misuse. And every action it takes needs to be attributable to something in your audit logs so that incidents can be investigated.
There are three fundamentally different identity models for agents, and the choice between them has major implications for security, auditability, and operational complexity.
- Agent-as-service: the agent has its own identity, independent of any user. It acts with permissions defined for its specific role. Best for agents that take actions on behalf of the system rather than specific users.
- Agent-as-user-delegate: the agent acts with the identity and permissions of the user who triggered it. Full user capabilities, but no real separation between agent actions and user actions in audit logs.
- Agent-as-trusted-intermediary: the agent has its own identity for authentication, but carries user identity context as a verified claim in its tokens. This lets resources make user-specific decisions while still attributing actions to the agent.
Service Accounts vs User Identity Delegation
The choice between service account identity and user identity delegation is the first and most important identity architecture decision for any agent system.
The service account model wins for almost every production use case. Create a service account specifically for each agent role, define the minimum permissions that account needs, and never expand those permissions beyond what is actively being used. The operational overhead of managing per-agent service accounts is justified by the security benefit. Every time.
OAuth 2.0 and Client Credentials for Agents
For agents that need to authenticate to external APIs, OAuth 2.0 with the client credentials grant is the standard pattern. The agent has a client ID and client secret, exchanges them for a short-lived access token with specific scopes, and uses that token for API calls. The token is scoped, short-lived, and auditable. That combination is what you want.
import httpx
from datetime import datetime, timedelta
from typing import Optional
import jwt
class AgentAuthClient:
"""OAuth 2.0 client credentials flow for agents."""
def __init__(
self,
auth_server_url: str,
client_id: str,
client_secret: str,
scopes: list[str],
):
self.auth_url = auth_server_url
self.client_id = client_id
self.client_secret = client_secret
self.scopes = scopes
self._token: Optional[str] = None
self._token_expires: Optional[datetime] = None
async def get_token(self) -> str:
"""Return a valid token, refreshing if expired."""
if self._token and self._token_expires and datetime.utcnow() < self._token_expires - timedelta(minutes=5):
return self._token
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.auth_url}/token",
data={
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
"scope": " ".join(self.scopes),
},
)
response.raise_for_status()
token_data = response.json()
self._token = token_data["access_token"]
self._token_expires = datetime.utcnow() + timedelta(seconds=token_data["expires_in"])
return self._token
async def authenticated_request(self, method: str, url: str, **kwargs) -> httpx.Response:
"""Make an authenticated request with automatic token refresh."""
token = await self.get_token()
headers = kwargs.pop("headers", {})
headers["Authorization"] = f"Bearer {token}"
async with httpx.AsyncClient() as client:
return await client.request(method, url, headers=headers, **kwargs)
# Per-agent auth client with scopes specific to this agent's role
research_agent_auth = AgentAuthClient(
auth_server_url="https://auth.yourcompany.com",
client_id="research-agent-prod",
client_secret=secrets.get("agents/research/client-secret"),
scopes=["read:documents", "read:search", "read:calendar"],
# Explicitly excluded: write:*, admin:*, delete:*
)Scoping Token Permissions Correctly
The scope list in an OAuth token defines what the bearer can do. For agents, scope design is a critical security decision. The principle is the same as least-privilege access: each agent role gets the minimum scopes required to perform its function, and nothing more.
# Scope definitions by agent role, explicit and minimal
AGENT_SCOPES = {
"research-agent": [
"read:documents",
"read:search",
"read:external_apis",
# NOT: write:*, delete:*, admin:*
],
"writer-agent": [
"read:documents",
"write:draft_content",
"write:user_notes",
# NOT: publish:*, send:email, delete:*
],
"notifier-agent": [
"send:notification",
"read:alert_config",
# NOT: read:documents, write:*, delete:*
],
"orchestrator-agent": [
"read:*", # orchestrator needs broad read to plan
"delegate:task", # can create sub-agent tasks
# NOT: write:*, delete:*, admin:*
# Even the orchestrator does not get write access
# Worker agents handle writes within their narrower scopes
],
}
def validate_scope_request(agent_role: str, requested_scopes: list[str]) -> list[str]:
"""Enforce scope limits per agent role."""
allowed = set(AGENT_SCOPES.get(agent_role, []))
granted = []
denied = []
for scope in requested_scopes:
if scope in allowed or any(scope.startswith(a.rstrip("*")) for a in allowed if a.endswith("*")):
granted.append(scope)
else:
denied.append(scope)
if denied:
audit_log.warning(f"Agent {agent_role} denied scopes: {denied}")
return grantedToken Lifecycle Management
Short-lived tokens are a core property of a well-designed agent authentication system. A token that expires after one hour limits the window of exploitation if it gets leaked or stolen. A token that never expires is a permanent credential that requires a full incident response to revoke.
- Set access token TTLs between 15 minutes and 1 hour for most agent roles. Short enough to limit exposure, long enough that refresh overhead is acceptable.
- Implement automatic token refresh before expiry. The 5-minute pre-expiry refresh window is standard practice.
- Store tokens only in memory, never on disk or in databases. An in-process cache is the correct storage location.
- Implement token revocation for incidents. Your identity provider should support immediate token invalidation for compromised service accounts.
- Rotate client secrets on a quarterly schedule or after any suspected compromise.
Identity in Multi-Agent Chains
When an orchestrator agent delegates to worker agents, identity doesn't automatically propagate. Each agent in the chain needs its own identity and must authenticate independently. But resource APIs may need to know which user's data is being processed, even if the immediate caller is a service account.
The solution is to pass user identity as a verified claim in the agent's token, separate from the agent's service account identity. The token says: 'I am the research-agent service account (authenticated), and I am processing data for user alice@company.com (claimed, verified by the issuing auth server).' Resource APIs can then apply user-specific access controls while still authenticating the agent independently.
# Token with both agent identity and user context claim
token_payload = {
# Agent service identity, authenticated via client credentials
"sub": "svc-research-agent",
"iss": "https://auth.yourcompany.com",
"exp": int(time.time()) + 3600,
# Scoped permissions for this agent role
"scope": "read:documents read:search",
# User context, verified by auth server, passed as claim
# Resource APIs use this for user-specific data access decisions
"act_as_user": "alice@company.com",
"act_as_user_id": "usr_abc123",
"delegation_reason": "user-initiated research task",
"task_id": "task_xyz789", # links to the original user request
# Signed by the auth server's private key
# Resource servers verify this with the auth server's public key
}Auditing Agent Actions by Identity
Every action an agent takes should be logged with enough identity context to answer three questions during an incident investigation: which agent took this action, on whose behalf, and as part of which task? Without answers to all three, incident response becomes a manual forensics exercise. That's not where you want to be at 2am.
from dataclasses import dataclass
from datetime import datetime
@dataclass
class AgentAuditEvent:
timestamp: datetime
agent_id: str # "svc-research-agent"
agent_version: str # "1.4.2"
user_id: str # "usr_abc123" (whose data)
task_id: str # "task_xyz789" (which task)
trace_id: str # distributed trace ID
action: str # "tool_call:web_search"
resource: str # "https://api.search.com/v1/search"
params_hash: str # SHA256 of params (not plaintext, may contain sensitive data)
outcome: str # "success" | "denied" | "error"
duration_ms: int
def audit_tool_call(agent_ctx, tool_name: str, params: dict, result: dict):
import hashlib
event = AgentAuditEvent(
timestamp=datetime.utcnow(),
agent_id=agent_ctx.agent_id,
agent_version=agent_ctx.version,
user_id=agent_ctx.acting_for_user,
task_id=agent_ctx.task_id,
trace_id=agent_ctx.trace_id,
action=f"tool_call:{tool_name}",
resource=params.get("url", tool_name),
params_hash=hashlib.sha256(json.dumps(params, sort_keys=True).encode()).hexdigest(),
outcome=result.get("status", "unknown"),
duration_ms=result.get("duration_ms", 0),
)
audit_logger.info(asdict(event))Conclusion
Agent identity is not a detail to figure out after the system is built. It's a foundation that everything else depends on: access control, audit logging, incident response, and compliance all require clear, consistent identity attribution for every agent action. The patterns here, service account identity per agent role, scoped short-lived tokens, user context as a verified claim, and structured audit logging, provide that foundation.
When something goes wrong with an agent, and something always eventually goes wrong, the quality of your identity architecture will determine whether you can investigate and contain the incident quickly or spend days reconstructing what happened from incomplete logs.
Frequently Asked Questions
Build secure agentic systems with AgentixForce
We help companies design, build, and secure production-grade AI agent systems. From architecture reviews to full implementation, our team brings deep expertise to every engagement.