datarekha

Token Theft in AI Agents — Credential-Blind Execution

Keep API keys and OAuth tokens out of model context, then authorize every tool call at runtime. Build a safe fake-token lab and learn vault injection, least privilege, audience restriction, DPoP, and approval gates.

10 min read Intermediate Agentic AI Lesson 56 of 71

What you'll learn

  • Why a secret in model context is already outside the security boundary
  • How credential-blind tool execution keeps raw tokens away from the model
  • How scope, audience, lifetime, and proof of possession limit stolen-token replay
  • Why authorization must run after planning but before dispatch
  • How to test token-exfiltration defenses using fake credentials only

Before you start

An AI agent usually touches a credential in one of three places: its prompt, the process environment, or a tool executor. Only the third location is a good default. Prompts are observable by the model and may appear in traces. Process-wide environment variables are invisible to the model until you give it a shell, debugger, file reader, or error log that can expose them. A narrow executor can keep the secret behind a typed operation.

This lesson uses fake strings such as tok_demo_read_7f3a. Never paste a real credential into a prompt, playground, test fixture, screenshot, or trace.

The attack path

Token theft in an agent is normally a chain, not one bug:

  1. An attacker controls text the agent will read: an email, issue, page, PDF, retrieved chunk, or tool result.
  2. The text induces the model to request a dangerous action or disclose data.
  3. A raw credential is visible to the model, or an overpowered tool silently attaches it.
  4. The system has an egress channel: HTTP, email, a pull request, a rendered image URL, a generated file, or logs visible to another party.

The first step is prompt injection. The third and fourth steps turn it into a breach. This is why “tell the model not to reveal secrets” is not a security control. The model must not possess the secret in the first place.

Model / plannersees untrusted textsees no raw tokenPolicy engineuser + action + scopeallow / deny / approveTool executorfetches scoped tokencalls one APIResourcere-checks scopeand audienceintentpermitrequesttoken and result are redacted before logs or model context
The model chooses an action; it never becomes the credential store or the authorization server.

Put policy between plan and action

The model can propose create_ticket(customer_id, summary). It cannot decide whether the current user may create that ticket, select a broader OAuth scope, or attach a bearer token. Those are deterministic checks.

from dataclasses import dataclass

@dataclass(frozen=True)
class Action:
    tool: str
    operation: str
    resource: str
    external_write: bool = False

def authorize(user, action):
    allowed = user["scopes"]
    required = f"{action.tool}:{action.operation}"
    if required not in allowed:
        return "deny"
    if action.external_write:
        return "approve"  # pause for a human
    return "allow"

The important placement is after the model produces a structured action and before the executor dispatches it. An input-only guardrail cannot see the final tool arguments. An output-only secret scanner may catch a literal token, but it cannot tell whether delete_repository("prod") was authorized.

Four properties every agent token needs

The IETF’s OAuth 2.0 Security Best Current Practice (RFC 9700) gives us a useful design vocabulary:

PropertyQuestionWhy it limits damage
ScopeWhat operations may it perform?A read token cannot delete or send.
AudienceWhich resource server accepts it?A token for Drive is rejected by Slack.
LifetimeHow long is it useful?Short-lived access narrows the replay window.
Sender constraintWhich client proves it owns the token?DPoP (RFC 9449) or mTLS-bound tokens resist replay by a thief who has only the token string.

RFC 9700 recommends minimum required privileges, audience restriction, and sender-constrained access tokens where feasible. Refresh tokens for public clients must be sender-constrained or rotated. These controls do not excuse a leak: they make a leaked credential less reusable and less powerful.

Safe lab: can the request leave the sandbox?

This lab has no network call and no real secret — only the deterministic policy that sits between a (possibly injected) plan and dispatch. Before reading the output, predict each verdict: the attacker’s exfiltration send, a read aimed at an un-allowlisted destination, a read whose payload smuggles the fake secret, and an ordinary read. Which gate catches each one?

from dataclasses import dataclass

@dataclass
class Request:
    operation: str
    destination: str
    data: str

FAKE_SECRET = "tok_demo_read_7f3a"  # inert test string
user_scopes = {"tickets:read"}
approved_destinations = {"support.internal.example"}

def decide(req, human_approved=False):
    required = "tickets:write" if req.operation == "send" else "tickets:read"
    if required not in user_scopes:
        return "DENY: missing " + required
    if req.destination not in approved_destinations:
        return "DENY: destination is not allowlisted"
    if FAKE_SECRET in req.data:
        return "DENY: fake-secret detector fired"
    if req.operation == "send" and not human_approved:
        return "PAUSE: external write needs approval"
    return "ALLOW"

# The injected attacker's request tries to exfiltrate via an external "send".
attack = Request("send", "collect.attacker.example", FAKE_SECRET)
print("attack         ->", decide(attack))
print("read, bad dest ->", decide(Request("read", "collect.attacker.example", "ticket #42")))
print("read, secret   ->", decide(Request("read", "support.internal.example", FAKE_SECRET)))
print("legit read     ->", decide(Request("read", "support.internal.example", "ticket #42")))
print("The executor never received a real credential.")
attack         -> DENY: missing tickets:write
read, bad dest -> DENY: destination is not allowlisted
read, secret   -> DENY: fake-secret detector fired
legit read     -> ALLOW
The executor never received a real credential.

The gates are ordered, and each is independent. The attacker’s send never even reaches the destination or secret checks — it lacks the scope, so least privilege stops it first. (A request that did hold tickets:write and tried an external send would fall through to the last gate, PAUSE: external write needs approval.) Every verdict comes from deterministic policy on the typed action — not the model’s confidence, and not a raw credential, which the executor attaches only after ALLOW.

Production checklist

  • Keep secrets out of prompts, tool schemas, model-visible environment dumps, exception text, and traces.
  • Let a server-side executor fetch a per-user, per-tool credential only after policy allows the typed action.
  • Default tools to read-only; split broad tools into narrow operations.
  • Bind tokens to the smallest scope and audience; prefer short lifetimes and rotation. Use DPoP or mTLS where the ecosystem supports it.
  • Re-authorize at the resource server. The agent’s decision is never proof of the user’s permission.
  • Redact request headers, query strings, tool results, and errors before they enter logs or model context.
  • Put sends, deletes, payments, deployments, and permission changes behind an explicit approval gate with the exact action and destination shown.
  • Test with canary strings and fake services. Measure leakage rate, task success, false positives, approval burden, and added latency.

In one breath

  • A secret in model context is already outside the security boundary — a malicious doc, email, or tool result can make the model repeat it, so “tell the model not to reveal secrets” is not a control.
  • Credential-blind execution: the model proposes a typed action, deterministic policy authorizes it, and only a trusted executor injects the scoped credential for that one API call.
  • Run authorization after planning, before dispatch, on the final structured action and arguments — an input guardrail can’t see them, an output scanner can’t tell whether the action was authorized.
  • Give every agent token four properties — scope, audience, lifetime, sender-constraint (DPoP/mTLS) — to blunt a stolen token’s replay; PKCE doesn’t help once a bearer token is taken.
  • Re-authorize at the resource server, redact secrets/results out of logs and context, default tools to read-only, and gate sends/deletes/payments behind explicit approval — the model’s decision is never proof of permission.

Quick check

Quick check

0/3
Q1Where should a raw API credential normally enter an agent workflow?
Q2What does an OAuth audience restriction do?
Q3An input scanner accepts a request, but the model later proposes deleting production data. What control is still required?

Next

Credentials are only half the boundary. The next lesson covers what happens when the tool catalogue itself lies: MCP tool poisoning, shadowing, and rug pulls.

Sign in to track your progress

Completed lessons, your XP, level, and streak save to your account — it's free and takes a few seconds.

Practice this in an interview

All questions
How would you prevent an AI agent from leaking or misusing API credentials?

Keep raw credentials outside model context and traces. Let the model propose typed intent, authorize the final action and arguments deterministically, then have a trusted executor inject a short-lived, narrowly scoped, audience-restricted credential for one call. Re-authorize downstream and gate high-impact writes with explicit approval.

Does PKCE protect an AI agent after an OAuth access token has been stolen?

No. PKCE protects the authorization-code exchange by binding it to the client that initiated the flow. It does not make an already issued bearer access token unusable. Limit stolen-token replay with audience restriction, short lifetime, downstream authorization and sender-constrained tokens such as DPoP or mTLS.

What are the major security risks of deploying autonomous agents?

Key risks include prompt injection, especially indirect injection via tool or retrieval outputs, hijacking the agent, excessive tool permissions enabling damaging actions, data exfiltration, confused-deputy privilege escalation, and unbounded loops driving cost or harm. Mitigations include least-privilege tools, sandboxing, input and output guardrails, human-in-the-loop approval for sensitive actions, and audit logging.

What is the confused deputy problem in agent systems, and how does it relate to agent-to-agent authentication?

A confused deputy occurs when an agent uses its elevated permissions to perform an action on behalf of a less-privileged caller that the caller could not do directly, leading to privilege escalation. The root cause is that a trusted agent acts on natural-language requests, including from other agents, without verifying the originator's authority, so robust systems propagate identity and scope on every hop and enforce access control on agent-to-agent calls.

Related lessons

Explore further

Skip to content