All postsSecurity

AI Agent Auth in 2026: Why Per-User Delegated Access Is the Hard Part

Agent auth is harder than human SSO. Learn PKCE, token exchange, scope attenuation, delegation chains and token refresh at scale, plus a reference architecture.

14 min readHoook Team

For a decade, "auth" in a SaaS product meant one thing: a human signs in, gets a session, and clicks buttons. In 2026 the account that touches your customers' data is increasingly not a person. It is an agent that reads a user's inbox, updates their CRM, files a ticket and posts a summary to Slack, all from a single prompt, often while the user is doing something else. The trend is clear: agent products are moving from shared service accounts to per-user delegated access, where every action runs with a specific person's permissions, and teams are discovering that this is much harder to build than single sign-on.

This post covers why, the protocol pieces that matter, the lesson of the August 2025 Salesloft Drift breach, and a reference architecture with a checklist.

Why agent auth is harder than human SSO

SSO answers one question: who is this person? The human is present, moves at human speed, and can be asked to re-authenticate.

An agent breaks each of those assumptions.

  • There are at least two identities in every request. The agent (a piece of software you registered as an OAuth client) and the user it is acting for. Arcade's guide to multi-user agent auth puts the rule well: an agent's effective permissions should be the intersection of what the user can do and what the agent is allowed to do, never the union.
  • The user is often absent. Agents run in the background, on schedules, or in response to webhooks. They need long-lived access, which means refresh tokens, which means storage.
  • The code path is not fixed. A classic integration calls the same five endpoints every time. An agent decides what to call at runtime, based on text that may include untrusted content. Nango's guide to agent API authentication points out that prompt injection turns an over-privileged agent into a tool for the attacker, and that credentials also leak through error messages, debug logs and tool outputs that echo request headers.
  • The agent talks to many APIs. One task can touch Gmail, Salesforce, GitHub and Jira. Each has its own OAuth server, scope model, token lifetime and refresh quirks.

So the problem is not "log the agent in." It is: for every tool call, prove which user authorized it, prove which agent is making it, limit it to what this task needs, and keep the credentials out of reach of the model.

The lesson from the Salesloft Drift breach

If you want one incident to explain why this matters, it is the Salesloft Drift campaign of August 2025.

Drift, an AI chat product owned by Salesloft, held OAuth tokens that let it read and write data in its customers' Salesforce orgs. According to Google Threat Intelligence Group, a threat actor tracked as UNC6395 used compromised OAuth tokens tied to the Drift integration to access Salesforce customer instances from as early as August 8 to at least August 18, 2025. The attacker ran queries against objects such as Accounts, Cases, Opportunities and Users, then searched the exported data for secrets like AWS access keys and Snowflake tokens. GTIG assessed that the primary intent was to harvest credentials. The actor deleted query jobs to cover their tracks, although the logs were not affected.

The blast radius was large. The Hacker News reported that more than 700 organizations were potentially impacted, and that the scope reached beyond Salesforce to other platforms integrated with Drift. GTIG's update confirmed that tokens for the "Drift Email" integration were also abused against Google Workspace accounts configured to use Drift, and advised customers to treat every token stored in or connected to Drift as potentially compromised. Salesloft and Salesforce revoked all active access and refresh tokens for the Drift application, as Arctic Wolf's summary notes.

How did the attacker get the tokens? Salesloft's Mandiant-led investigation, reported by Help Net Security on September 8, 2025, found that the actor had access to Salesloft's GitHub account from March through June 2025, where they downloaded repository content, added a guest user and set up workflows. They then got into Drift's AWS environment and obtained OAuth tokens for Drift customers' technology integrations.

Three lessons for anyone building an agent product:

  1. Your token store is the target. Nobody phished 700 Salesforce admins. The attacker compromised one vendor that held tokens for all of them. If you store per-user tokens, you are that vendor.
  2. Broad scopes turn a breach into a data export. The tokens could run bulk queries across core CRM objects. A token limited to what the chatbot actually needed would have exposed far less. Google's remediation advice included restricting connected app scopes to the minimum.
  3. Valid tokens look like normal traffic. The attacker did not break OAuth. They used it. Detection depended on unusual user agents, Tor exit nodes and query patterns, which means you need per-token audit trails and the ability to revoke by integration, by tenant and by user, fast.

Public clients, PKCE and where agents live

OAuth distinguishes confidential clients, which can keep a secret (a backend server), from public clients, which cannot (a CLI, a desktop app, a browser extension, a local MCP client). Many agents are public clients. They run on a developer's laptop or inside a desktop app, so any embedded client secret should be assumed extractable.

PKCE (Proof Key for Code Exchange) is the answer. The client generates a random code_verifier, sends a hash of it (code_challenge) with the authorization request, and proves possession of the original value when it redeems the code. An attacker who intercepts the code cannot use it.

This is no longer optional guidance. RFC 9700, the OAuth 2.0 Security Best Current Practice published in January 2025, says public clients MUST use PKCE and recommends it for confidential clients too. It also says refresh tokens for public clients MUST be sender-constrained or rotated. The MCP authorization specification goes further for agents: MCP clients must include a resource parameter (RFC 8707) so tokens are bound to a specific server, and per its security considerations they must use the S256 challenge method and refuse to proceed if the authorization server does not advertise PKCE support.

Here is the shape of a PKCE authorization request an agent client builds:

import crypto from "node:crypto";
 
const verifier = crypto.randomBytes(32).toString("base64url");
const challenge = crypto
  .createHash("sha256")
  .update(verifier)
  .digest("base64url");
 
const url = new URL("https://auth.example.com/authorize");
url.search = new URLSearchParams({
  response_type: "code",
  client_id: CLIENT_ID,
  redirect_uri: "https://agent.example.com/oauth/callback",
  scope: "calendar.readonly",
  code_challenge: challenge,
  code_challenge_method: "S256",
  resource: "https://mcp.example.com",
  state: crypto.randomUUID(),
}).toString();
 
// Store verifier + state server-side, keyed to the signed-in app user,
// then redirect. Never let the model see either value.

Bind the flow to the verified user in your app, so a callback cannot attach an account to the wrong user, and keep the verifier and tokens on the server, outside the agent's context window.

Token exchange, scope attenuation and delegation chains

Once a user has granted access, the next mistake is to hand the agent that full grant for every task. The user's token was issued for "everything this app might do." A single task needs much less.

RFC 8693, OAuth 2.0 Token Exchange, gives you a standard way to trade one token for another. The client posts to the token endpoint with the grant type urn:ietf:params:oauth:grant-type:token-exchange, a subject_token representing the user, and optionally an actor_token representing the agent. The authorization server returns a new token that can be narrower in scope, shorter-lived and restricted to one audience. Strata's analysis of agentic OAuth describes this as swapping a long-lived human token for a task-specific one, which is the core of scope attenuation.

POST /token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
 
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=<user access token>
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&actor_token=<agent workload token>
&actor_token_type=urn:ietf:params:oauth:token-type:jwt
&scope=calendar.readonly
&resource=https://calendar.example.com

RFC 8693 also draws a line that agent builders should care about: delegation versus impersonation. With impersonation, the agent becomes indistinguishable from the user. With delegation, the agent keeps its own identity and it is explicit that it is acting for the user. The spec's act claim records the acting party, and nested act claims record a chain of prior actors, with the most recent on the outside. That is exactly what you need when a planner agent hands work to a sub-agent that calls a tool server:

{
  "sub": "user:[email protected]",
  "aud": "https://calendar.example.com",
  "scope": "calendar.readonly",
  "act": {
    "sub": "agent:scheduler-subagent",
    "act": { "sub": "agent:planner" }
  }
}

Each hop should only ever reduce scope. If a sub-agent can obtain more privilege than its parent, you have built a confused deputy. The MCP spec makes one version of this rule explicit: an MCP server that calls upstream APIs must use a separate token for them and must not pass through the token it received from the client.

A practical note: many SaaS providers do not support token exchange today. You cannot ask Salesforce or Google to mint an attenuated token for you. In that case, enforce attenuation in your own tool layer. The stored token stays broad, but the tool gateway only exposes the operations a task was granted, and checks policy before every call.

Machine-speed authorization

Humans authorize once and act a few times a minute. Agents can make dozens of calls per second across many users. That changes the operational side of auth.

  • Consent has to be incremental. Asking for every scope at onboarding produces blanket grants nobody reads. The current MCP spec describes a step-up flow: a server returns 403 with error="insufficient_scope" and the scopes needed, and the client re-authorizes with the union of old and new scopes. Build your product so an agent can pause, ask for one more permission, and continue.
  • Not every action is equal. Reading a calendar and wiring money should not share a policy. Arcade suggests a read, draft and commit gradient, where high-impact commits need a human approval step. Nango makes the same point about reserving approval for high-impact actions.
  • Revocation must be near real time. When a user leaves a company or an integration is compromised, cached tokens and long-running agent jobs must stop. Strata highlights continuous access evaluation for this reason. At minimum, check a revocation list at the tool gateway on every call, not only at token issuance.
  • Policy runs before the call, not in the prompt. "Please don't delete anything" in a system prompt is not an access control. The check belongs in code the model cannot rewrite.

Token storage and refresh at scale

Per-user access means you now hold N users times M providers worth of credentials. This is where most of the unglamorous engineering lives.

Concern What goes wrong What to do
Storage Tokens in plain database columns or logs Encrypt at rest with envelope encryption, keys in a KMS, strict access paths
Exposure Tokens in prompts, tool outputs or error messages Inject credentials server-side at call time; the model only sees results
Refresh Many workers refresh the same token at once Lock per connection, refresh ahead of expiry, handle rotated refresh tokens atomically
Rotation Old refresh token reused after rotation Persist the new token before using the new access token; alert on reuse
Failure Revoked or expired grants fail silently Mark the connection broken, notify the user, pause dependent jobs
Audit No link between an API call and a user Log user, agent, tenant, scope, tool and outcome for every request

The refresh race deserves emphasis. When a provider rotates refresh tokens, two workers refreshing the same connection concurrently can leave you holding an invalidated token, and the user sees a "please reconnect" prompt for no visible reason. A single-flight refresh per connection, with a distributed lock and a short grace window, fixes most of this.

Then there are per-provider quirks: refresh tokens that expire after inactivity, scopes that change on refresh, rate-limited token endpoints. Each is small. Across hundreds of providers, they add up.

A reference architecture for per-user agent auth

Pulling this together, a sound design has five parts:

  1. App identity layer. Your users sign in with your normal auth (OIDC, SSO). Every agent session is bound to a verified app user and tenant.
  2. Connect flow. A hosted authorization flow per provider, using authorization code with PKCE, exact redirect URIs, state checks, and a resource parameter where supported. The callback attaches the grant to the verified user, never to whoever happens to complete it.
  3. Credential vault. Encrypted, per-user, per-provider token storage with automatic refresh, rotation handling, revocation and health status. Only the gateway can read from it.
  4. Tool gateway. The only component that makes outbound API calls. For each tool call it resolves the user and agent, evaluates policy (scopes granted for this task, action tier, rate limits), obtains an attenuated token through exchange if the provider supports it, injects credentials, calls the API, and redacts secrets from the response. MCP servers sit here, one per user or per connection, validating audience and never passing tokens through.
  5. Audit and control plane. Immutable logs with the full delegation chain, anomaly detection on volume and query shape, and kill switches to revoke by user, tenant, provider or integration.
User -> App (OIDC session) -> Agent runtime
                                  |
                          tool call (user_id, agent_id, task)
                                  v
                            Tool gateway --policy--> Policy engine
                                  |
                          fetch/refresh token
                                  v
                           Credential vault (KMS)
                                  |
                                  v
                          Provider API (scoped token)
                                  |
                                  v
                           Audit log + alerts

The model never holds a token. It asks for an action, and the gateway decides whether that user, through that agent, may do it right now.

You can build all of this yourself, and some teams should. Others would rather not maintain OAuth apps, refresh logic and provider quirks for hundreds of services. Managed integration layers such as Hoook handle the connect flow, encrypted token storage and automatic refresh, and expose per-user hosted MCP servers and an API proxy, so your team can focus on the policy and approval logic that is specific to your product.

Checklist: per-user auth for agent products

Use this in design review before an agent touches customer data.

Identity and consent

  • Every agent action is tied to a verified app user and tenant
  • Agent and user are separate identities in logs and tokens (delegation, not impersonation)
  • OAuth callbacks are bound to the user who started the flow
  • Scopes are requested incrementally, with step-up for new permissions

Protocol hygiene

  • Authorization code flow with PKCE (S256) for every client, public or confidential
  • Exact redirect URI matching and state validation
  • resource parameter sent and audience validated on every token
  • No token passthrough between services; each hop gets its own token
  • Token exchange or gateway-level attenuation so each task gets the minimum scope

Storage and refresh

  • Tokens encrypted at rest with keys in a KMS
  • Tokens never appear in prompts, tool outputs, logs or error messages
  • Single-flight refresh with locking and correct rotation handling
  • Broken connections detected and surfaced to the user

Runtime control

  • Policy checked in code before every tool call
  • High-impact actions require human approval
  • Rate limits per user, per agent and per provider
  • Revocation by user, tenant, provider and integration takes effect within minutes

Detection and response

  • Audit log records user, agent chain, scope, tool, endpoint and outcome
  • Alerts on unusual volume, new user agents, new IP ranges and bulk exports
  • A tested runbook for "our token store may be compromised"

The Drift breach showed what happens when a vendor holds broad, long-lived tokens for hundreds of customers. The standards to do better already exist: PKCE, resource indicators, token exchange, delegation claims and refresh token rotation. The work is applying them consistently, per user and per provider, and keeping the model away from the credentials. Get that right, and your agent can act for users without becoming the next integration in the news.

Book a demo

Thirty minutes on what you are building, with the engineers behind Hoook.

Ask Ivy nowOur assistant answers straight away, then passes you to the team.

We answer within one business day.

Powered bydayrun.ai