How Auth0 Token Vault solved the multi-provider problem for AI agents and the three lessons I'd love to share with the Auth0 team.
It started with a dream setup.
I was building MCS (Model Context Standard), an open-source Python SDK that lets AI agents interact with services like Gmail, Google Drive, and Slack through a standardized tool interface. The agent doesn't know it's talking to Gmail. It just calls search_emails(query="invoices from last week") and gets results.
Authentication should be equally invisible. The agent shouldn't care how it gets a token. It just needs one.
Then I found Auth0 Token Vault and immediately understood the promise.
The Promise That Hooked Me
Before Token Vault, every service my agent needed meant another OAuth client. Gmail? Register a Google OAuth app. GitHub? Another OAuth app. Slack? Another one. Each with its own token refresh logic, its own scopes, its own error handling. Multiply that by every agent deployment.
Token Vault flips this. One Auth0 login. One refresh token. Access to every connected service.
token = provider.get_token("gmail") # → Google access token
token = provider.get_token("github") # → GitHub access token
token = provider.get_token("slack") # → Slack access token
Without new OAuth clients, new callback URLs and new token refresh logic. You configure connections in the Auth0 dashboard, and the agent accesses any service through a single method call. The code never changes.
Then I tried to run it.
The Wall: Where Agents Live vs. Where OAuth Expects Them
My agent runs in a Docker container. No browser. No localhost callback. No web UI. Just a terminal.
And Claude Code lives in its sandbox.
The most natural auth pattern for this is Device Flow: show a URL and a code, the user authenticates after following the URL, done. It's the pattern every smart TV and CLI tool uses.
Here's where I hit the wall:
Token Vault requires a Confidential Client. That's a Regular Web Application with a client secret. Makes sense, you don't want agents exchanging tokens without proper credentials.
Device Flow requires a Native Application. That's a public client, no secret. Also makes sense, it's designed for devices without secure storage.
You can't have both on the same application.
This isn't a misconfiguration. It's a product constraint that makes perfect sense from a security perspective, but it creates a real gap for AI agents. Agents are confidential (they have secure storage for secrets), but they behave like devices (no browser, no callback URL).
Auth0 clearly sees this gap. It's exactly why they launched Auth for GenAI. But today, with the current Token Vault, I needed a bridge.
Building the Bridge
What if I bring the Device Flow experience to Token Vault, without actually using Device Flow?
The user experience I wanted:
Agent: I need Gmail access. Please open this URL and enter code ABCD-1234.
User: *opens URL, enters / checks code, logs in with Google*
Agent: Got it. Reading your emails now.
Under the hood, it's actually a full OAuth Authorization Code Flow with PKCE, which is compatible with Token Vault. The user just doesn't see that. They see a URL and a code, exactly like Device Flow.
To make this work, I needed three things:
- A broker that can receive the OAuth callback on behalf of the agent
- A way for the agent to poll for completion without a callback server
- Zero-knowledge encryption so the broker never sees credentials in plaintext
That became LinkAuth - An open-source credential broker that gives any sandboxed agent a Device Flow UX on top of standard OAuth and more...
The Architecture (Where SOLID Actually Matters)
MCS uses a layered design where each layer has exactly one job:
┌─────────────────────────────────────────────────┐
│ AI Agent (LLM + Tools) │
│ "Search my Gmail for invoices" │
├─────────────────────────────────────────────────┤
│ MailDriver + AuthMixin │
│ Intercepts AuthChallenge, shows URL to user │
├─────────────────────────────────────────────────┤
│ Auth0Provider (CredentialProvider) │
│ get_token("gmail") → Token Vault exchange │
├─────────────────────────────────────────────────┤
│ LinkAuthConnector (AuthPort) │
│ Device-flow UX via broker │
├─────────────────────────────────────────────────┤
│ LinkAuth Broker │
│ Receives OAuth callback, encrypts, stores │
└─────────────────────────────────────────────────┘
The critical design decision: AuthPort is a protocol, not a base class.
class AuthPort(Protocol):
def authenticate(self, scope: str, *, url: str | None = None, ...) -> str: ...
Any object with an authenticate method satisfies it. No inheritance. No imports. The provider doesn't know and doesn't care which connector is plugged in.
This means switching auth strategies is a one-line change:
provider = Auth0Provider(
domain="my-tenant.auth0.com",
client_id="...",
client_secret="...",
_auth=connector, # OAuthConnector OR LinkAuthConnector - same interface
)
# This line is the same regardless of auth method:
token = provider.get_token("gmail")
Switch connector and you switch from browser login to device-flow UX. Zero changes in agent code. Zero changes in the Gmail driver. Zero changes in the LLM prompt.
The Double Flow Surprise
This one cost me an entire day and it's the kind of insight you only get from building against an API, not reading about it.
Auth0 has a connection setting called "Authentication and Connected Accounts." The name suggests a single step. Log in with Google, Auth0 stores the Google token in Token Vault. One flow, done.
In practice, the first time a user connects, they go through two separate flows:
- OAuth login - The user authenticates with Auth0 (via Google). You get an Auth0 refresh token.
-
Connected Accounts setup - You exchange the refresh token for a My Account API token (MRRT), POST to
/connect, the user consents again in a browser, then POST to/complete.
Two browser interactions. Two consent screens. For what the user perceives as "log in with Google."
First run: get_token("gmail")
→ No refresh token → AuthChallenge("Open URL, enter ABCD")
→ User logs in → Auth0 refresh token ✅
→ Token Vault → "federated_connection_refresh_token_not_found"
→ MRRT exchange → POST /connect → AuthChallenge("Open URL again")
→ User consents (again!) → POST /complete ✅
Every subsequent call: get_token("gmail")
→ Refresh token cached → Token Vault → Google access token ✅
→ Instant.
Once set up, the experience is seamless. But that first run is a surprise for both developers and users. To absorb this complexity, Auth0Provider handles the entire state machine automatically. The agent developer never sees the MRRT exchange or the /connect flow, just AuthChallenge exceptions that bubble up as "please open this URL" messages.
The Checklist Nobody Gives You
If you're integrating Auth0 Token Vault for agents, here's the checklist I wish the docs had as a single page:
- [ ] Application type: Regular Web Application (Confidential Client)
- [ ] Grant types: Authorization Code, Refresh Token, Token Vault
- [ ] Connection mode: "Authentication and Connected Accounts" on your Google connection
- [ ] API settings: Allow Offline Access enabled on your API
- [ ] MRRT: Multi-Resource Refresh Tokens enabled (Tenant Settings → Advanced)
- [ ] Audience: Pass the correct
audienceparameter in your authorization request - [ ] Google Cloud: Gmail API enabled in the GCP project linked to your Google OAuth client
- [ ] Scopes: Include
offline_accessin your Auth0 scopes, AND the Gmail scope in yourconnection_scopes
Miss any one of these and you get a cryptic error. federated_connection_refresh_token_not_found is the one you'll see most. It's the catch-all for "something in the chain isn't configured right." A dedicated "Token Vault Setup Wizard" in the Auth0 dashboard could save developers hours.
What Auth0 Gets Right - And Three Ideas to Make It Even Better
Token Vault is the right abstraction for AI agents. The idea that an agent holds one credential and accesses dozens of services through configuration rather than code is exactly how agent auth should work. No other identity platform offers this today as far as I know.
Building this integration gave me three pieces of feedback I'd love to share with the Auth0 team:
1. Bridge the Device Flow Gap
Agents are confidential clients that behave like devices. They have secure storage for secrets (so Device Flow's public-client model isn't needed), but they can't receive callbacks or open browsers (so Authorization Code Flow is awkward). A first-party "Agent Flow" - Device Flow UX backed by a Confidential Client - would eliminate the need for broker infrastructure like LinkAuth entirely. The UX works. We proved it. Auth0 could offer it natively.
Or use LinkAuth in the meantime ;-)
2. Streamline the Double Flow
"Authentication and Connected Accounts" is the right feature, but the first-run experience of two separate consent screens is confusing. If the consent for Connected Accounts could be bundled into the initial login flow (even as an optional "eager connect" mode), the first-run experience would match what the setting name already implies: one flow, authentication and connected accounts.
3. One Page, All the Settings
Token Vault touches Auth0 application settings, API settings, connection settings, tenant-level feature flags, and the external provider's dashboard. A single "Token Vault Setup Guide" or setup wizard that walks through all eight configuration steps in sequence would save every developer the evening I spent chasing federated_connection_refresh_token_not_found through five different settings screens.
Try It
The Gmail Agent example is a fully working chat client that reads and sends email through any LLM. Auth0 Token Vault is one flag away:
# 1. Install
pip install mcs-driver-mail[gmail] mcs-auth-auth0 litellm rich python-dotenv
# 2. Configure (.env)
AUTH0_DOMAIN=my-tenant.auth0.com
AUTH0_CLIENT_ID=...
AUTH0_CLIENT_SECRET=...
# 3. Run with Auth0 + LinkAuth (device-flow UX, works in Docker/CLI)
python main.py --auth0-linkauth
# Or with Auth0 + browser login (dev machine)
python main.py --auth0-oauth
That's it. The agent starts, the LLM discovers the Gmail tools, and on the first call that needs credentials, the user sees a URL to authenticate. From the second call on, Token Vault handles everything silently.
The code that makes this work is surprisingly short. The entire agent class is one line:
class GmailAgent(AuthMixin, MailDriver):
pass
AuthMixin intercepts any AuthChallenge and turns it into a message the LLM can show the user. MailDriver provides the Gmail tools. The agent itself has zero auth logic.
Switch --auth0-linkauth to --auth0-oauth and the auth path changes from device-flow to browser login. The driver, the LLM prompt, the tool definitions - nothing changes.
Everything is open source:
- MCS Python SDK - The agent framework with pluggable auth
- LinkAuth - The credential broker that bridges Device Flow UX to Token Vault
- Gmail Agent Example - The full working example
Built for the Auth0 AI Agent Hackathon. Token Vault is the right idea. I just needed to build a bridge to reach it.

Top comments (0)