Skip to content
datarekha
MLOps Hard Asked at MicrosoftAsked at GoogleAsked at Okta

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

The short answer

No. PKCE protects the authorization-code exchange, not a copied bearer access token. Reduce replay risk with audience and scope restrictions, short-lived tokens, resource-server authorization, and sender-constrained tokens such as DPoP or mTLS.

How to think about it

No—not for an ordinary bearer access token. PKCE protects the authorization code before tokens are issued; it does not bind the resulting access token to the AI agent. If an attacker copies a bearer token, they can usually replay it at the intended API until it expires or is otherwise rejected.

Why PKCE helps, and what it actually proves

PKCE means Proof Key for Code Exchange. It is a protection for OAuth’s authorization-code flow, where an OAuth client is software requesting access, an authorization server issues tokens, and a resource server is the API that accepts them.

The agent starts by generating a random secret called the code_verifier. It sends only a derived value, the code_challenge, in the authorization request:

code_challenge = BASE64URL(SHA-256(code_verifier))

The user signs in and approves access. The authorization server sends an authorization code back through the browser redirect. The agent then calls the token endpoint with the code and the original code_verifier. The authorization server hashes the verifier, compares it with the recorded challenge, and issues tokens only when they match.

An attacker who intercepts the authorization code therefore has an incomplete credential. The attacker has the code but not the verifier. The exchange fails.

That is the attack PKCE solves: authorization-code interception. PKCE proves possession of the verifier for that particular code exchange. It does not prove possession of the verifier on every later API request, and the verifier is not normally sent to the resource server at all.

Common trap: state, nonce, and PKCE are different controls. state correlates an authorization response with a client session and helps prevent request-forgery attacks. nonce binds an OpenID Connect ID token to an authentication request. PKCE protects the authorization code. None of them turns an ordinary access token into a sender-constrained credential.

What changes after the token is issued

A conventional access token is usually a bearer token. “Bearer” means that possession is enough: whoever presents the token can use it, much like whoever holds cash can spend it. The API does not normally know whether the presenter completed the original OAuth flow.

A resource server receiving a request might validate the token’s signature or ask the authorization server to introspect it. It can check the issuer, audience, expiry, and scopes. But it generally does not repeat the PKCE exchange or ask for the original code_verifier.

That creates the key distinction:

  • PKCE answers, “Who may redeem this authorization code?”
  • A bearer access token answers, “Does the presenter currently possess a valid token?”

Once the attacker has the second credential, the first question is over.

Concrete example

Imagine a customer-support AI agent that can read tickets in a CRM system. At 09:00, the user authorizes the agent. The authorization server issues a token with these effective properties:

  • audience: crm-api
  • subject: user-1842
  • scope: tickets.read
  • expiry: 09:15

At 09:04, an observability trace accidentally records the Authorization header. An attacker copies the token and sends:

GET /v1/tickets/4815 HTTP/1.1
Host: crm.example
Authorization: Bearer stolen-access-token

The CRM API sees a correctly signed, unexpired token intended for crm-api, with permission to read tickets for that user. It may return 200 OK. The attacker never saw the authorization code and never knew the PKCE verifier. That does not matter: the token endpoint was not involved in this request.

At 09:15, the token should be rejected as expired. Until then, PKCE does nothing to stop replay. If the attacker also steals a refresh token, the exposure may last much longer, depending on refresh-token rotation, revocation, and the authorization server’s policies.

An opaque access token behaves the same way from the attacker’s perspective. The resource server may validate it through introspection instead of checking a JWT locally, but it still accepts the credential according to its current validity and permissions.

Controls that address stolen-token replay

ControlWhat it limitsWhat it does not solve
Audience validationUse of the token at an unrelated APIReplay at the intended API
Narrow scopesThe operations available to the thiefThe thief using an allowed operation
Resource-level authorizationAccess to the wrong tenant, ticket, or recordPossession of the token itself
Short access-token lifetimeThe replay windowReplay before expiry
DPoP or mTLSUse without the bound key or certificateTheft of both token and key

The resource server should validate the token’s issuer, signature or introspection result, audience, expiry, and scopes. Audience answers “which service may accept this token?” Scope answers “which broad operations may it perform?” Neither answers “is this the original agent presenting it?”

The API also needs downstream authorization. A scope such as tickets.read should not automatically grant access to every ticket in every tenant. The server should still check the subject, tenant, object, and business action. This reduces the blast radius when a valid token is abused, although it does not prevent abuse within the permitted boundary.

Short lifetimes are useful because they reduce the time available for replay. They are not a cure. A token stolen at 09:01 can still be used at 09:02. Choose the lifetime alongside refresh-token handling, user experience, and the damage possible during the window.

Sender-constrained tokens change the answer more substantially. With DPoP, the client creates a key pair and proves possession of the private key with a signed proof on each request. The access token is bound to the public key. An attacker who steals only the token cannot create a valid proof. With mutual TLS, or mTLS, the token is bound to a client certificate and the resource server verifies the certificate during the TLS connection. mTLS is often a better fit for controlled service-to-service traffic; DPoP is generally more flexible for application-level clients.

Both require real support from the authorization server and resource server. If the API silently accepts the token as an ordinary bearer token, the binding is decorative. And if an attacker compromises the agent process and steals both the access token and its private DPoP key, sender constraint cannot rescue that process.

The AI-agent wrinkle

An AI agent creates extra places for credentials to leak: prompts, tool arguments, model traces, debug logs, screenshots, and third-party connectors. PKCE does not protect any of them after the agent has received the token.

A safer design keeps the token outside the model’s context. The model requests a narrowly defined tool such as read_ticket; a server-side tool or credential broker adds the authorization header and returns only the permitted result. The raw token should not appear in prompts, tool output, conversation history, or ordinary traces. Authorization headers must be redacted, and refresh tokens need stricter storage and rotation than short-lived access tokens.

A useful incident symptom is successful API calls from an unfamiliar network or user agent, with no suspicious token exchange at the authorization server. That often means the theft happened after issuance—from a log, runtime, connector, or model-visible context—not during the PKCE-protected exchange.

PKCE is still essential. Without it, an attacker who intercepts the authorization code may obtain a token in the first place. It is defense in depth, not a general-purpose stolen-token defense.

What they’ll ask next

Does a client secret solve this instead of PKCE?
No. A client secret authenticates a confidential client to the token endpoint. PKCE protects the code exchange, especially for public clients. Neither prevents replay of an already issued bearer token. A secret embedded in an AI agent or mobile application should be assumed recoverable.

Are audience and scope restrictions enough?
No. They reduce where the token works and what it can do, which is valuable blast-radius reduction. They do not distinguish the legitimate agent from an attacker at the intended API. You need sender constraint, shorter validity, stronger downstream authorization, or some combination.

Would DPoP make the agent safe after token theft?
It makes a copied token alone insufficient, provided the resource server validates the DPoP proof and the private key remains protected. It does not help if the attacker controls the agent runtime, steals the private key too, or can make the legitimate agent perform arbitrary requests.

Say this in the interview

“PKCE stops an attacker from exchanging a stolen authorization code, but it does not stop replay of an already issued bearer access token; for that, I would use least privilege, audience and resource-level checks, short lifetimes, careful agent-side secret handling, and sender-constrained tokens such as DPoP or mTLS.”

Learn it properly Token theft & runtime authorization

Keep practising

All MLOps questions

Explore further