DEV Community

Mukesh
Mukesh

Posted on

Auth0 Token Vault: Letting an Autonomous Agent Call Slack and Google APIs Without Ever Holding the User's Credentials

Give an autonomous agent a Slack bot token or a Google OAuth refresh token and you've created a standing liability: that credential now lives in your agent's environment variables, its vector store, or worse, its prompt history, and it works until someone rotates it. The credential is long-lived, broadly scoped, and — because agents retry, resume, and get replayed for debugging — it ends up copy-pasted into logs more often than any human-issued token ever would.

Auth0's Token Vault, part of the "Auth for GenAI" surface released through 2025, solves a narrower but more common problem: your agent doesn't need the user's Google or Slack credential at all. It needs an access token, scoped to one API, valid for one call, that Auth0 mints on demand from a federated connection it already manages. The agent asks Auth0 for a token to call Google Calendar; Auth0 exchanges the user's stored federated grant for a short-lived access token and hands back only that. The refresh token, the client secret, the whole OAuth dance — none of it ever reaches the agent process.

This is a different problem from CIBA-style async approval, where a human has to say yes to an action in real time. Token Vault is about routine, already-authorized delegated access — the agent doesn't need permission each time, it needs a token each time, and those are not the same thing.

The shape of the problem without it

Say you're building an agent that summarizes a user's unread Slack messages every morning. The naive setup: user does an OAuth flow once, you store the resulting xoxp- token in your database, and every morning your cron job pulls it out and calls Slack's API directly.

The failure modes compound fast:

  • The token is scoped to everything the user granted at signup, not to what today's job needs.
  • If your database leaks, every stored token leaks with it — no expiry saves you if the token itself is long-lived.
  • Revoking access means finding and deleting rows across however many tables store tokens, and you'll miss one.
  • Debugging "why did the agent's Slack call fail" means grepping logs that may contain the token itself if a request/response was ever dumped for troubleshooting.

What Token Vault changes

With Token Vault, your app never stores the Slack or Google token. Instead:

  1. The user connects Google/Slack through Auth0's normal federated connection flow, once.
  2. Auth0 stores the resulting tokens itself, inside the vault, tied to the user's Auth0 identity.
  3. Your agent, holding only a valid Auth0-issued access token for your API (not the third party's), calls Auth0's token exchange endpoint to get a short-lived, connection-scoped token.
  4. That token is used for exactly the downstream call, then discarded.

The @auth0/ai SDK wraps this as getAccessTokenForConnection, callable from inside a tool the agent invokes:

import { getAccessTokenForConnection } from "@auth0/ai-sdk";
import { AccessTokenForConnectionError } from "@auth0/ai/interrupts";

async function summarizeUnreadSlack(userId) {
  let accessToken;
  try {
    accessToken = await getAccessTokenForConnection({
      connection: "sign-in-with-slack",
      // scopes are requested per call, not baked into a static grant
      scopes: ["channels:history", "channels:read"],
      userId,
    });
  } catch (err) {
    if (err instanceof AccessTokenForConnectionError) {
      // the user hasn't connected Slack, or the grant was revoked —
      // surface a re-auth prompt instead of crashing the job
      return { needsReconnect: true };
    }
    throw err;
  }

  const res = await fetch("https://slack.com/api/conversations.history", {
    headers: { Authorization: `Bearer ${accessToken}` },
  });
  return res.json();
}
Enter fullscreen mode Exit fullscreen mode

The important part isn't the fetch call — it's what's not in this function. There's no refresh-token storage, no manual POST /oauth/token with a client secret, and no long-lived Slack credential sitting in your database for an attacker to find. If this job runs on a schedule and the user revoked Slack access three days ago, getAccessTokenForConnection throws instead of quietly using a stale token, which is the behavior you actually want from a job you're not watching.

Scoping per call, not per grant

The scopes array above is worth dwelling on. Most integrations request the union of every scope they'll ever need at signup, because asking twice is annoying for a human clicking through a consent screen. An agent doesn't have that constraint — it can request channels:read for a summarization job and chat:write only for the specific tool call that posts a reply, and Token Vault will mint tokens matching exactly what was asked, bounded by what the user originally granted.

That matters operationally: if you're running multiple agent tools against the same connection, each with a narrower token, a bug in your summarizer tool literally cannot post messages, because it never held a token capable of it. You get least-privilege enforcement for free, per invocation, without threading scope-checking logic through your own code.

Handling revocation gracefully

The failure case that actually matters for unattended agents is mid-run revocation — the user disconnects Slack in their account settings while your nightly job is asleep. Without Token Vault, your stored token either silently 401s (and your retry logic treats it as a transient failure, burning retries against an account nobody's going to reconnect) or, worse, still technically works because you never expired it.

With Token Vault, revocation happens at Auth0, so the very next getAccessTokenForConnection call throws AccessTokenForConnectionError immediately — no stale token, no ambiguous 401 to interpret. The fix in your agent code is to catch that specific error and route it to a "needs reconnect" state rather than your generic retry-and-backoff path:

if (err instanceof AccessTokenForConnectionError) {
  await markConnectionStale(userId, "slack");
  return; // don't retry — retrying a revoked grant just wastes cycles
}
Enter fullscreen mode Exit fullscreen mode

That one branch is the difference between an agent that fails loudly and correctly, and one that silently retries a dead connection every night until someone notices the summaries stopped coming.

Where this fits

Token Vault isn't a replacement for user-facing OAuth consent, and it isn't the right tool if your agent needs a human to approve each individual action — that's CIBA's job. It's specifically for the case in between: access that's already been granted once, needs to be exercised repeatedly and unattended, and should never require your infrastructure to become a credential store. If you're building any agent that calls a third-party API on a schedule rather than in direct response to a user click, that's the exact shape of problem Token Vault was built for — and the integration cost is one SDK call, not a token-storage subsystem.

Top comments (0)