Skip to content

Agent Identity and Non-Human Authentication

CoreConceptJuly 29, 20269 min read

A human who logs into a payments dashboard gets a session, a timeout, and a moment of hesitation before clicking submit. An agent that calls the same payments API gets none of that. It authenticates once, then fires dozens of calls a minute with nobody watching each one, so whatever credential it presents has to carry the entire authorization decision by itself — because nothing downstream will catch a mistake the way a human's second thought would.

This guide is for engineers who already understand API keys and OAuth and want the delta that shows up once the caller is autonomous. We will follow one agent, invoice-agent, which reads uploaded vendor invoices and calls a payments API to schedule payouts. The running problem: a long-lived, broadly-scoped shared API key is the wrong credential for that job, and what replaces it — scoped short-lived tokens, workload identity, delegated OAuth token exchange, and cryptographic attestation — has to answer questions a static secret never could. This is a credential-design problem, distinct from the model being tricked into misusing a credential it legitimately holds; for that mechanism, see Prompt Injection in RAG and Tool-Using Agents.

Agent identity map: authentication, authorization, delegation, and attestation answer four different questions
Agent identity map: authentication, authorization, delegation, and attestation answer four different questions

Why Agent Credentials Are a Different Problem

Human authentication and authorization are well-trodden ground: a login proves identity, a session carries it forward, and a policy check decides what the session may do on each request. What's easy to overlook is that a human also supplies an implicit control that never shows up in any diagram — judgment before the click. Nobody automates approving their own six-figure payout at 3 a.m. That judgment call is part of the security boundary even though no code enforces it.

Agents remove that judgment call by design. invoice-agent might process forty invoices in a shift, each ending in a call to POST /v1/payouts, with no person reviewing any individual one. That means the credential it presents has to do work a human's session never had to do alone: prove which workload is calling (authentication), constrain what that call may do (authorization), establish whose authority it's acting under (delegation — is it acting as itself, or on behalf of the accounts-payable team that queued the invoice?), and ideally prove which specific task and code produced this exact call (attestation). A shared static API key answers the first question weakly — "someone who holds the key" — and effectively nothing else, because rotating a narrower key per task is operationally annoying enough that teams default to one broad, long-lived secret.

A human's session gates each click; an agent's credential gates every call it will ever make
A human's session gates each click; an agent's credential gates every call it will ever make

Quick reference

  • Authentication answers 'which workload,' authorization answers 'what may it do' — a static key conflates both into one secret with no separation.
  • Delegation matters even for a single-purpose agent: is invoice-agent acting as itself, or on behalf of the user/team that queued the task?
  • Attestation is the newest piece: proof a specific code build processed a specific task, not just possession of a bearer string.
  • A human session implicitly assumes judgment before each action; an agent has no equivalent unless the credential or policy layer supplies one explicitly.
  • If two unrelated tasks can reuse the same credential, the resource server cannot tell them apart after the fact, in logs or in an incident.

Remember this

A credential built for a human session proves who logged in once; an agent credential has to prove who's calling and what this specific call is allowed to do, because no one is watching each call happen.

How Scoped, Short-Lived Credentials Replace the Static Key

The replacement for a standing shared secret is workload identity: an identity the platform issues to the running process itself — a container, a service account, an attested build — rather than a string checked into config or an environment variable. There is nothing to leak from a file, because there is no long-lived secret sitting in one. invoice-agent's orchestrator holds this kind of identity and uses it, not to call the payments API directly, but to authenticate to a token service.

From there, delegated OAuth token exchange (the token-exchange grant defined in RFC 8693) mints the credential that actually reaches the payments API. The orchestrator exchanges its own workload identity for a narrow token: audience restricted to payments-api, scoped to payouts:write, carrying claims for the exact task id, vendor, and amount ceiling, with a TTL measured in minutes instead of months. The payments API validates those claims before it executes anything — a call for the wrong vendor, past the TTL, or over the ceiling is rejected regardless of whether the bearer token is otherwise well-formed, because the boundary lives in the token's claims, not in an internal allowlist somewhere else.

invoice-agent requests a payout: a scoped, short-lived token replaces the standing API key
invoice-agent requests a payout: a scoped, short-lived token replaces the standing API key

Quick reference

  • Workload identity is platform-issued to the running process (SPIFFE/SVID-style, or cloud workload identity federation) — there's no static secret to steal from config.
  • Token exchange (RFC 8693) lets the orchestrator swap its own identity for a narrower, task-scoped token instead of holding one master key for every downstream call.
  • Bind claims the resource server actually checks: audience, task id, scope, and an amount or resource ceiling — not just 'this bearer is well-formed.'
  • Keep TTLs close to task duration; a five-minute token minted for one payout is inconvenient to steal and cheap to let expire unused.
  • Per-agent service principals are a reasonable interim step when full workload identity isn't available yet — see OAuth 2.0 and OpenID Connect Explained for the underlying delegation model.
Static key call (invoice-agent, before)
1// Every invoice-agent run reuses the same long-lived secret,2// scoped to "all payouts" because narrower keys were too much overhead.3const res = await fetch("https://payments-api.internal/v1/payouts", {4  method: "POST",5  headers: { authorization: `Bearer ${process.env.INVOICE_AGENT_KEY}` },6  body: JSON.stringify({ vendorId, amountCents }),7});
Task-scoped token exchange (invoice-agent, after)
1// Orchestrator's workload identity is exchanged for a token2// scoped to exactly one task — nothing to leak, nothing to over-scope.3const { access_token, expires_in } = await tokenService.exchange({4  grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",5  subject_token: workloadIdentityToken,6  audience: "payments-api",7  scope: "payouts:write",8  claims: { taskId: "INV-4471", vendorId: "vnd_882", amountCeilingCents: 340000 },9  ttl_seconds: 300,10});11 12const res = await fetch("https://payments-api.internal/v1/payouts", {13  method: "POST",14  headers: { authorization: `Bearer ${access_token}` },15  body: JSON.stringify({ vendorId: "vnd_882", amountCents: 340000, taskId: "INV-4471" }),16});

Remember this

The fix isn't a stronger secret, it's a narrower one: mint a token per task with an audience, a scope, and a task claim the resource server checks, so possessing the bearer string alone stops being enough.

Static Keys vs Service Principals vs Workload Identity vs Attestation

None of these models is free, which is exactly why static keys survive as long as they do. A shared secret is operationally the simplest thing to build — one string, no token service, easy to debug when something breaks — and that simplicity is why early agent deployments default to it. A per-agent service principal is a modest upgrade: invoice-agent gets its own identity, so revoking a compromised credential doesn't take down an unrelated support-agent sharing the same key. But the credential itself is still valid indefinitely and still authorizes every call the agent will ever make, forever, until someone remembers to rotate it.

Workload identity paired with token exchange is the model actually replacing shared keys in production agent systems, because it fixes both axes at once: blast radius shrinks to one agent (service principal) and to one task (short TTL, task claim). Attestation goes further still — a signed statement about which code and which task produced a call, so the resource server can refuse not just "wrong scope" but "not a genuine invoice-agent run at all." The cost is real: a token service becomes a dependency the payments API must trust, and claim-checking means the payments team, not only the platform team, has to validate audience and task claims on every call. Reach for attestation when an agent can move money or touch regulated data; a read-only reporting agent rarely needs it.

Match the model to what a leaked or replayed credential could still do.
ModelLifetime & scopeMain weakness
Static API keyLong-lived, usually broadAny holder can act as the agent indefinitely
Per-agent service principalLong-lived, scoped per agentStill a standing secret; blast radius shrinks to one agent, not to one task
Workload identity + token exchangeMinutes, task/audience scopedNeeds a token service, and resource servers must check the claims
Cryptographic attestationBound to one signed executionNeeds a verifier on the resource server; heavier to operate
Four ways to authenticate invoice-agent: static key, service principal, workload identity, attestation
Four ways to authenticate invoice-agent: static key, service principal, workload identity, attestation

Quick reference

  • Static key: cheapest to build, most expensive to recover from — a leak means rotating the standing key everywhere it's baked in.
  • Service principal: fixes blast radius across agents, not blast radius across time — the credential is still good forever.
  • Workload identity + token exchange: fixes both, at the cost of running (and trusting) a token service.
  • Attestation: proves what actually ran, useful when identity alone isn't enough to rule out a tampered build or a replayed task.
  • None of these substitutes for authorization logic in the resource server itself — see API Security Best Practices for the checks that still belong there.
  • The rigor is the same one human auth already demands — see Sessions, JWTs, and OAuth/OIDC — just applied with no human left to catch a mistake.

Remember this

Choose the credential model by what a leaked or replayed token could still do: a read-only agent can live with a service principal; an agent that moves money needs task-scoped, short-lived tokens the resource server actually checks.

When invoice-agent's Key Gets Exfiltrated

Trigger. invoice-agent originally authenticated with one long-lived API key, provisioned at deploy time and read from an environment variable. A staging deployment mirroring production traffic for debugging left request logging turned one level too verbose, and a full Authorization header — the static key included — landed in a log aggregator several engineers could read. Symptom. Two weeks later, the payments team notices payouts to a vendor no invoice ever referenced, issued at 3 a.m. when no invoice-processing job was scheduled to run.

Root mechanism. The payments API had no way to distinguish that call from a legitimate one, because nothing about it could. invoice-agent's key authorized any vendor, any amount, indefinitely, with no claim tying a given call to a specific invoice or task. Whoever read that log line could replay the exact bearer token invoice-agent uses for every real payout — the key was the entire authorization boundary, and it drew no line between "invoice-agent processing INV-4471 right now" and "anyone with a copied header, at any time."

Diagnostic. The on-call engineer pulls the payments API audit log filtered by the leaked key's client id and diffs it against the orchestrator's task queue; any payout with no matching task record is suspect. Recovery. Revoke the compromised key at the identity provider immediately, so future replays fail outright, then reconcile every payout issued under that key during the exposure window — flag and reverse the ones with no legitimate task behind them. Prevention. Move invoice-agent to workload identity and per-task token exchange: a leaked token now expires in minutes, is scoped to one vendor and one amount ceiling, and carries a taskId claim, so a payout with no matching task is rejected at the payments API instead of discovered afterward in an audit log.

Failure detail: a leaked static key has no task binding, so the payments API cannot tell the replay from the real call
Failure detail: a leaked static key has no task binding, so the payments API cannot tell the replay from the real call

Quick reference

  • Trigger: verbose staging logging captured a full Authorization header including the static key.
  • Symptom: payouts to vendors, and at hours, no legitimate invoice-processing task explains.
  • Root mechanism: the key alone was the authorization boundary — no task claim, no TTL, no vendor ceiling for the API to check.
  • Diagnostic: diff the payments API audit log (by key or client id) against the orchestrator's task queue for unmatched payouts.
  • Recovery: revoke the key at the identity provider first, then reconcile and reverse payouts with no matching task.
  • Prevention: task-scoped, short-lived tokens with a taskId claim turn a replayed credential into a rejected call instead of a silent payout.

Remember this

A leaked long-lived credential is dangerous in exact proportion to what it can still do after the leak — task-scoped, short-lived tokens turn 'key exposed' from an open-ended incident into a five-minute window.

Key takeaway

Long-lived shared API keys work fine for a human who logs in once a day and clicks through a UI with a moment of hesitation built in. They fail for agents because nothing stands between possessing the secret and taking the action — no session timeout a person notices, no click a person pauses before. Replace the key's authority with claims a resource server actually checks: which task, which audience, how long, how much. Workload identity plus token exchange is the standard mechanism for that; per-agent service principals are a reasonable interim step; attestation earns its cost once an agent can move money or touch regulated data. If your agents also read untrusted content before deciding what to call, pair this with Guardrails and Sandboxing for AI Agents — identity limits what a credential can do, guardrails limit what a compromised decision can do.

Practice (25 min): build a tiny token-exchange stub for one existing agent-to-API call. Starter: write a mintTaskToken(taskId, vendorId, amountCeilingCents, ttlSeconds) function returning a signed JWT with aud, taskId, vendorId, amountCeilingCents, and exp claims, plus a middleware that rejects any call whose aud doesn't match the API, whose exp has passed, or whose request body doesn't match the token's vendorId/amount. Expected success: a token minted for INV-4471 / vnd_882 / 340000 cents is accepted for that exact payout. Intentional break: replay the same token for vnd_995, or replay it after the TTL expires. Recovery: confirm the middleware rejects both with a typed claim_mismatch or token_expired code instead of a generic 401. Pass when a stolen or reused token cannot authorize a call outside the task it was minted for.

Share:

Related Articles

A mobile app adds "Sign in with Provider," gets back a token, and calls it a login. OAuth 2.0 was never built to answer

Read

Prompt injection is what happens when untrusted text tries to steer the model away from the developer's intended instruc

Read

An AI agent becomes risky the moment it can read private data, call tools, write files, send messages, or trigger busine

Read

Keep learning

Follow a structured path or browse all courses to go deeper.