DEV Community

pickuma
pickuma

Posted on • Originally published at pickuma.com

Authenticating AI Agents: API Keys vs OAuth Device Flow vs Scoped Tokens

Your agent needs to hit the GitHub API, your internal deploy service, and a customer's Postgres. Nobody is at a keyboard. Whatever credential you hand it has to keep working through a 3am retry, survive a rotation, and leave a trail that tells you which run did what.

The three options you actually choose between — a static API key, the OAuth 2.0 device authorization grant (RFC 8628), and short-lived scoped tokens — are not three flavors of the same idea. Each answers a different question about who is present when the credential is minted and who is accountable when it is used. Picking the wrong one does not fail loudly on day one. It fails on the day you need to revoke something, and discover the credential is copy-pasted into four CI configs and a developer's shell profile.

What each mechanism actually assumes

A static API key assumes the secret is the identity. There is no user, no session, no expiry. Whoever holds the string is the caller. That is genuinely fine for a narrow class of cases: a single-tenant background job, an internal service where the blast radius is already bounded, a local dev loop. It is not fine the moment the key can act on behalf of more than one principal, because the token carries no answer to "on whose behalf?" Your audit log records the key, and the key is the same for every run.

The operational tax is rotation. A static key has no natural refresh point, so rotating it means finding every place it was copied to. Agents make this worse than normal service-to-service calls, because agent frameworks encourage stuffing credentials into environment files, MCP server configs, and tool definitions that get shared across machines.

The OAuth 2.0 device authorization grant assumes a human is reachable, just not on this device. That is the whole point of RFC 8628: the client is input-constrained (a TV, a CLI, a headless box), so it displays a code, the human opens a browser somewhere else, approves, and the device polls the token endpoint until it gets an access token and refresh token.

The assumption that matters is reachable human. Device flow is not a machine-to-machine grant. It is a delegation grant with a deferred consent screen. If your agent runs on a schedule with nobody watching, device flow only works because a human ran it once, weeks ago, and the refresh token has been quietly rolling over ever since. That is a legitimate design — it is roughly how CLI tools like gh auth login behave — but be clear about what you built: an agent that inherits a specific person's authority, indefinitely, with that person's name on every action in the audit log.

Short-lived scoped tokens assume the agent has its own identity, and that the identity is separable from the credential. The credential is minted on demand, narrow in scope, and expires in minutes. In OAuth terms this is the client credentials grant (RFC 6749 §4.4) when the agent acts as itself, or token exchange (RFC 8693) when it needs to act on behalf of a user for one specific call. In cloud terms it is workload identity federation — the agent proves what it is via a platform-issued attestation and trades that for a scoped access token. SPIFFE/SPIRE is the vendor-neutral version of the same shape.

This is the model that fits non-human callers, because it is the only one where "the agent" and "the agent's current credential" are different objects. You can revoke one without hunting for the other.

Device flow with a long-lived refresh token is the most common accidental design in agent tooling, because it is the easiest to demo. The failure mode is quiet: the agent keeps operating with a departed employee's scopes long after their SSO session, laptop, and badge are gone. If you use device flow for an unattended agent, treat the refresh token as a standing grant that needs its own expiry policy and its own review — not as an implementation detail of login.

Pick by who is present, then by what can be scoped

Run the decision in two passes.

First pass — who is present at authorization time?

  • A human, at the moment of the call. Use a normal authorization code flow with PKCE in the surrounding app and pass a per-request token down to the agent. Do not promote it to a stored credential.
  • A human once, then never again. Device flow, with an explicit refresh-token lifetime and a re-consent interval. Write down what happens when that person leaves.
  • Nobody, ever. The agent is a workload. Give it a workload identity and mint scoped tokens per task. Client credentials or federation, not a key file.

Second pass — can the authority be narrowed to the task?

This is where most implementations stop early, and it is the part that actually limits damage. A token scoped to repo is not scoped. A token scoped to one repository, write access to one branch, valid for eight minutes, is scoped. GitHub App installation tokens are a good reference implementation: they expire in an hour and can be restricted to specific repositories and permissions at request time. Cloud STS tokens support similar narrowing through session policies and duration limits.

For agents specifically, add two properties that human-facing OAuth rarely needs:

  1. Per-run distinctness. Each agent run should be traceable to its own token, so a compromised or misbehaving run is bounded and identifiable. A single long-lived token shared across runs collapses your audit log into one row.
  2. Sender constraint. Bearer tokens are replayable by anyone who obtains them, and agents leak them through logs, traces, and prompt context more readily than normal services do. DPoP (RFC 9449) or mTLS-bound tokens (RFC 8705) bind the token to a key the holder must prove possession of, so a stolen token alone is not enough.

The Model Context Protocol authorization spec pushed the ecosystem toward this shape — MCP servers act as OAuth resource servers, and clients are expected to obtain tokens with an explicit audience rather than accept a shared secret. If you are building agent tooling now, matching that pattern costs you little and keeps you compatible with where the tooling is heading.

What to do if you already shipped API keys

You probably did, because it is what every SDK quickstart hands you. The migration does not have to be a rewrite.

Start by putting a token broker between your agents and the static keys. The agent authenticates to the broker with a workload identity, the broker holds the upstream long-lived credential, and it issues a short-lived, narrowly scoped token per task. Nothing upstream changes. You get expiry, per-run attribution, and a single place to revoke — which is most of the benefit of the full model.

Then fix the audit gap. Add a run identifier that travels with every call the agent makes, and make sure it lands in the same place your upstream logs land. If you cannot answer "which agent run deleted this record?" from logs alone, the credential design is not finished, regardless of which grant type you chose.

Last, set an expiry on anything that currently has none. A key that never expires is a key nobody ever tests the rotation path for, and rotation paths that have never been exercised do not work when you need them at 3am.


Originally published at pickuma.com. Subscribe to the RSS or follow @pickuma.bsky.social for new reviews.

Top comments (0)