Inspiration
Auth0 Token Vault solves a hard problem elegantly: how do you give an AI agent delegated access to third-party APIs without handing it long-lived credentials? Short-lived, scoped, revocable tokens minted via federated connection exchange. The right answer.
But while building with Token Vault, we kept asking a question it was never designed to answer: once a valid token is issued, what guarantees the agent uses it for the action the user actually approved? A prompt-injected agent passes every Auth0 check, gets a valid Google access token, and silently mutates the tool call payload to exfiltrate credentials through a legitimate API. Authentication is correct. Authorization is correct. Execution is compromised.
We built LICITRA × Auth0 Gateway to prove this gap is real, demonstrate it against a production API, and show what closing it looks like.
What it does
LICITRA × Auth0 Gateway enforces one invariant at runtime, per execution:
AUTHORIZED_REQUEST == EXECUTED_REQUEST
Every tool call passes through two independent verification layers:
- Auth0 verifies identity and delegated access (Token Vault exchange).
- LICITRA verifies the executed payload is byte-identical to the authorized payload (canonical JSON SHA-256).
If either fails, the API call is blocked before any external side effect occurs.
The system runs a real Claude Sonnet agent that schedules Google Calendar events through Auth0 Token Vault. A runtime toggle switches LICITRA enforcement on and off — same agent, same prompt injection, same Auth0 session. The only variable is execution integrity.
- LICITRA OFF: The agent embeds Auth0-derived credentials into the calendar event description. Token Vault exchanges the token. A malicious event is created on the user's real Google Calendar with stolen credentials in plaintext. Every auth check passes.
- LICITRA ON: Same attack. LICITRA detects payload mutation via canonical hash comparison. Execution is denied before the Google API call fires. The calendar stays clean.
Three additional button-driven scenarios demonstrate the full enforcement model: happy path (allow), mutation attack (deny), and replay attack (deny via burned JTI).
Security model & user control
Permission boundaries. The agent's only delegated capability is create_event against the user's Google Calendar via Auth0 Token Vault. No other Google scopes are requested. The agent cannot read existing events, modify other calendars, access Gmail, or call any API outside the explicitly authorized Token Vault scope.
Consent transparency. Users authenticate via Auth0 Universal Login and explicitly grant Google Calendar access through Auth0's Connected Accounts flow with prompt=consent. The frontend displays "✓ Google Calendar connected" so users always know which provider access has been granted. Disconnecting in Auth0 immediately revokes the agent's ability to call the API.
High-stakes action protection. Every tool invocation is treated as a high-stakes action by default — it must pass both Auth0 (identity + delegated access) and LICITRA (payload integrity) before any external side effect. Failed integrity checks return signed evidence (authorized hash, executed hash, JTI) that can be audited or used to trigger downstream alerts.
Step-up authentication readiness. The LICITRA enforcement layer is designed to integrate with Auth0's step-up authentication: any tool call flagged as high-risk (e.g., events with external attendees, calendar modifications outside business hours, or detected payload mutations) can trigger an Auth0 step-up challenge before execution proceeds. This is the next planned integration — the gateway already produces the signal; the Auth0 challenge wiring is the next step.
Scope minimization. The Auth0 access token issued to the SPA is scoped only to the LICITRA gateway audience (https://licitra-gateway-api). The Google access token returned by Token Vault is held server-side, used once per execution, and never exposed to the browser or the LLM agent.
How we built it
Frontend: React 18 + Vite + Auth0 SPA SDK. Auth0 Universal Login (Google social connection) → SPA receives an access token scoped to the backend API → user interacts with a chat UI and the LICITRA toggle.
Backend: FastAPI + Anthropic Claude API with tool calling. Validates Auth0 JWTs (RS256 via JWKS), runs the agent, and gates every tool call through LICITRA before reaching Token Vault.
Token Vault integration: Backend exchanges the user's Auth0 access token for a Google access token via the federated connection token exchange grant (urn:auth0:params:oauth:grant-type:token-exchange:federated-connection-access-token), authenticated with Custom API Client credentials. The Google access token is then used to call Google Calendar API directly. prompt=consent and access_type=offline ensure Google issues a refresh token on first authorization.
LICITRA enforcement: Canonical JSON serialization (sorted keys, compact separators, UTF-8) → SHA-256 hash of the authorized payload → single-use JTI tokens for replay protection → hash comparison gate immediately before the Token Vault exchange. If the agent's actual output diverges from the authorized payload by a single byte, execution is denied. Adds roughly one millisecond to the request path.
No mocked endpoints. Every demo creates (or attempts to create) a real Google Calendar event via a Token Vault-issued access token.
Challenges we ran into
Refresh token handling. Google only issues a refresh token on first consent. If the Google connection was configured after a user's initial Auth0 login, Auth0 has no stored refresh token and Token Vault cannot mint future access tokens. Forced re-consent via prompt=consent in the authorization request fixed it.
Custom API Client vs SPA credentials. The Token Vault exchange must be authenticated with a Custom API Client, not the SPA. Initially we tried using the SPA's client credentials and got opaque 403s. Auth0's documentation on this is precise but easy to miss.
Making the demo deterministic. Live LLM agents are non-deterministic. For the demo to land reliably, we built button-based scenarios that bypass the LLM's randomness while still exercising the full Token Vault + LICITRA path. The natural-language agent path remains for the live exploit demonstration.
Defining the threat model precisely. The hardest part wasn't the code — it was articulating exactly which gap LICITRA closes and which it doesn't. LICITRA is not an Auth0 replacement. It's a complementary control class. Getting that framing right took more iterations than the implementation.
Accomplishments that we're proud of
- Real exfiltration, real API. The malicious calendar event is created on a real Google account through a real Token Vault flow. No simulation, no mocked surfaces. The exploit is reproducible.
- One invariant, one comparison. LICITRA's enforcement model is small enough to audit by hand: one canonical serializer, one hash function, one equality check, one replay guard. The minimalism is the security argument.
- Surfacing a structural gap, not just a bug. This isn't a vulnerability in Auth0. It's a control class that doesn't exist yet in any agent authorization stack we've seen. Naming the gap is part of the contribution.
What we learned
The trust boundary in classical OAuth lives between the user and the third-party API. The client is assumed to be software the user wrote or chose. That assumption silently breaks the moment an LLM agent becomes the client, because the agent's behavior is partially controlled by attacker-influenceable inputs (prompts, tool descriptions, retrieved documents).
Every authorization model designed before agents needs to be re-examined under this new assumption. Token Vault is not wrong — it's correct for the threat model it was designed against. The threat model itself has shifted, and the industry hasn't fully caught up.
We also learned how much of agent security is about making the invariants small enough to verify, not making the system big enough to handle every case. LICITRA's whole enforcement layer fits in roughly 200 lines of Python because the invariant it enforces is narrow: the executed request equals the authorized request. Nothing more.
What's next for LICITRA × Auth0 Gateway
Native integration into agent tool-calling flows. Today, an integrator has to bolt execution integrity on at the application layer. We'd love to see Token Vault expose a first-class hook for payload integrity verification between authorization and execution — register the canonical payload at authorization time, verify at exchange time. That single primitive would close this gap for the entire Auth0 ecosystem.
Ed25519-signed tickets. The current demo uses SHA-256 hash comparison. The production LICITRA core (linked as a submodule) uses Ed25519 signatures and a 5-gate verification pipeline with persistent JTI storage. Wiring the production core into the gateway is the next implementation step.
Multi-API enforcement. The demo uses Google Calendar. The same enforcement model applies to any Token Vault-mediated API: Gmail, Slack, Asana, HubSpot, GitHub. One LICITRA gateway can sit in front of all of them.
Threat-model documentation for the Auth0 community. The most valuable thing this project can produce isn't code — it's a clear, shareable artifact that names the gap, shows the exploit, and proposes the control class. We're planning a longer write-up.
📝 BONUS BLOG POST
What Token Vault Doesn't Protect: A Gap in the Agent Authorization Model
Auth0 Token Vault solves a problem that became urgent the moment AI agents started calling third-party APIs on behalf of users: how do you give an autonomous system delegated access without handing it long-lived credentials? Token Vault's answer — short-lived, scoped, revocable access tokens minted via federated connection exchange — is the right architectural answer. We use it in this project precisely because it is the right answer.
But building this hackathon project surfaced a question Token Vault was never designed to answer, and the answer matters more than we expected: once a valid token is issued, what guarantees the agent uses it for the action the user actually approved?
The honest answer is: nothing. And that gap is structural, not implementational.
Consider the threat model Token Vault assumes. The user is human. The client is software the user controls. The third-party API is external. Token Vault sits between them, ensuring the right credentials reach the right party. In this model, the client is trusted to use the token correctly because the user wrote the client (or chose it). The trust boundary lives between the user and the API.
Now insert an LLM agent into that flow. The "client" is no longer software the user controls — it's a probabilistic system whose behavior is partially determined by attacker-controllable inputs. A prompt injection in an email, a calendar invite, a document the agent reads — any of these can mutate the agent's tool call before it reaches the API. The token is still valid. The user is still authenticated. The Token Vault exchange still succeeds. And the API call that executes is not the one the user approved.
We built a working demonstration of this. A Claude Sonnet agent receives a clean user instruction ("schedule a team standup"), gets prompt-injected via its system context, and emits a tool call where the title is unchanged but the description contains an exfiltrated Auth0 access token. The Token Vault exchange runs normally. The Google Calendar API accepts the request. A real event is created on the user's real calendar with stolen credentials in plaintext. Every layer of Auth0's stack worked exactly as designed. The exfiltration happened inside a perfectly legitimate authorized API call, not outside it.
The fix isn't to harden Token Vault — Token Vault is doing its job. The fix is to add a control class that doesn't currently exist in agent authorization stacks: execution integrity verification. A guarantee that the payload hitting the API is byte-identical to the payload the user authorized.
Our implementation, LICITRA, does this with deterministic primitives: canonical JSON serialization (sorted keys, compact separators, UTF-8), SHA-256 hashing of the authorized payload, single-use JTI tokens for replay protection, and a hash comparison gate immediately before the Token Vault exchange. If the agent's actual output diverges from the authorized payload by a single byte, execution is denied. The mechanism is small, deterministic, and adds roughly one millisecond to the request path. It is cheap precisely because the invariant it enforces is narrow: the executed request equals the authorized request. Nothing more.
What we think this surfaces — and what we'd love to see Auth0 engineering consider — is whether agent tool-calling flows in Token Vault should have a first-class hook for payload integrity verification between authorization and execution. Today, an integrator like us has to bolt this on at the application layer. But the gap is universal: every agent using Token Vault has it, and most don't realize they have it. A native execution-integrity primitive in Token Vault — even something as simple as "register the canonical payload at authorization time, verify at exchange time" — would close it for the entire ecosystem.
Token Vault secures the channel. The next layer to build is the one that secures the content. Auth0 is uniquely positioned to define what that layer looks like, because Auth0 is uniquely positioned at the exact boundary where the gap exists.
Authentication didn't fail. Authorization didn't fail. Execution failed. That's the gap worth naming, and it's the gap we built this project to prove is real.
Built With
- anthropic
- auth0
- claude
- fastapi
- google-calendar-api
- javascript
- oauth2
- python
- react
- sha-256
- token-vault
- vite
Log in or sign up for Devpost to join the conversation.