---
title: "How to Connect Any OAuth2 Service to AI Agents with Auth0 Token Vault"
description: "Learn to connect any OAuth2 service to AI agents with Auth0 Token Vault's custom OAuth2 integration."
authors:
  - name: "Deepu K Sasidharan"
    url: "https://auth0.com/blog/authors/deepu-sasidharan/"
date: "Jun 10, 2026"
category: "AI"
tags: ["ai", "token-vault", "oauth2", "ai agents", "claude-code"]
url: "https://auth0.com/blog/connect-oauth2-service-to-ai-agents-with-auth0-token-vault/"
---

# How to Connect Any OAuth2 Service to AI Agents with Auth0 Token Vault

<style>
    
  /* Increases spacing between bullet points */   
    li {padding-bottom: .7em; }

</style>
*TL;DR: Auth0's custom OAuth2 integration works alongside built-in Token Vault connections to let you register services as custom social connections. The auth0-tv open-source CLI allows developers or AI agents like Claude Code to execute internal API calls with a single terminal command.*

Your AI agent (Claude Code, [OpenClaw](https://auth0.com/blog/openclaw-credential-problem/), etc.) can already call external services like Gmail, GitHub, Slack, Salesforce, and a growing list of other SaaS providers through [Auth0 Token Vault](https://auth0.com/ai/docs/intro/token-vault). That covers the public side of an enterprise stack. But most of the systems your users actually work with every day (the internal ticketing system, the company wiki, the deployment platform, the expense tool someone built in 2019) are not on that list. They are your own APIs, protected by OAuth2 and running behind your VPN.

You do not want to hardcode service accounts. You do not want to teach an agent framework about a brand-new credential store. You want those internal APIs to show up in the agent the same way Gmail does: as something it calls on the logged-in user's behalf, with tokens the agent never sees. That is exactly what Auth0's [custom OAuth2 integration for Token Vault](https://auth0.com/ai/docs/integrations/oauth2) is for. In this post, we will wire one up end-to-end, using a small sample helpdesk API and [Claude Code](https://www.anthropic.com/claude-code) as the agent.

## What We Will Learn in This Post

- How **Auth0's custom OAuth2 integration** fits alongside the built-in Token Vault connections, and when you'd reach for it.
- How to register an OAuth2-compliant service you own as a **custom social connection** and toggle on **Connected Accounts for Token Vault**.
- How to use [`auth0-tv`](https://github.com/deepu105/auth0-token-vault-cli), a small open-source CLI that drives Token Vault from the terminal or an AI agent, to call your internal API with one line, including calling it directly from Claude Code.

## Technology Stack

- [**Auth0 Token Vault**](https://auth0.com/ai/docs/intro/token-vault): [securely stores and refreshes OAuth tokens](https://auth0.com/blog/secure-ai-stop-managing-api-keys-by-hand/) for third-party services so your agent never handles credentials.
- [**Custom OAuth2 integration**](https://auth0.com/ai/docs/integrations/oauth2): Auth0's "bring your own provider" flavor of a social connection, for any OAuth2-compliant service that is not in the built-in list.
- [**`auth0-token-vault-cli`**](https://github.com/deepu105/auth0-token-vault-cli) (`auth0-tv`): an open-source Node.js CLI + [Agent Skill](https://agentskills.io/) that provides commands to login, `connect` to services, and to do authenticated `fetch` for the services. Designed for humans and AI agents alike.
- An AI agent that supports skills, like [**Claude Code**](https://www.anthropic.com/claude-code) or [OpenClaw](https://openclaw.ai).
- [**Acme Helpdesk sample**](https://github.com/deepu105/acme-helpdesk-oauth2-sample): a tiny Express + SQLite OAuth2 provider that plays the role of "your internal API" in this tutorial. Clone it, run it, do not worry about how it is built since that is not the point of this post. You can check the project's README.md if you are interested in knowing more.

## How It Fits Together

Here is the end-to-end flow we are about to wire up. It shows a user sending a request via Claude Code to the auth0-tv CLI.

<picture>
<img src="https://images.ctfassets.net/23aumh6u8s0i/79by7hroJCOtPIrWMTSHmc/5687e25182af492170a6e778a8850498/claude-auth0-tv-seq.png" alt="End-to-end flow showing a user sending a request via Claude Code. Claude calls the auth0-tv CLI, and it in turn calls Auth0 Token Vault to get tokens and makes authenticated requests to the Acme Helpdesk API" style="width:100%; margin: 1em auto; border: solid black 0px; border-radius: 0px;">
</picture>

The two pieces doing the heavy lifting are the ones you do not really see:

- **Token Vault** holds the [refresh token](https://auth0.com/docs/secure/tokens/refresh-tokens) for your Acme Helpdesk account, scoped to your Auth0 user. When `auth0-tv` requests an access token, Auth0 [exchanges the refresh token](https://auth0.com/blog/developers-guide-flexible-token-exchange-auth0/) with Acme Helpdesk's `/token` endpoint and returns a new one. You (and the agent) never handle either token directly.
- **The custom OAuth2 connection** is the Auth0-side record that tells Token Vault where to go and what to ask for. The same connection can be reused across any number of applications and agents in your tenant.

## Prerequisites

You will need the following tools and services to follow along:

- An [Auth0](https://a0.to/ai-content) account
- [Bun v1.2+](https://bun.sh/) or [Node.js v20+](https://nodejs.org/en/download)
- The latest version of [Auth0 CLI](https://github.com/auth0/auth0-cli) installed and logged in
- [`jq`](https://jqlang.org/) for composing and inspecting JSON payloads on the command line
- [Claude Code](https://www.anthropic.com/claude-code) or another AI agent installed and authenticated
- A way to expose `http://localhost:3001` publicly. [ngrok](https://ngrok.com/) is what I will use in this post; [Cloudflare](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) works just as well if you prefer. Auth0 needs to reach the sample app's `/authorize`, `/token`, and `/userinfo` endpoints.

## Getting Started

Clone the sample and start it:

```bash
git clone https://github.com/deepu105/acme-helpdesk-oauth2-sample.git
cd acme-helpdesk-oauth2-sample

bun install   # or npm install

cp .env.example .env.local
```

Open `.env.local` and update the fields.

First, fill in the three secrets. Use `openssl rand -hex 32` command to generate three unique secrets for this.

Next, you need to set `OAUTH2_REDIRECT_URI` to your Auth0 tenant's callback URL. Auth0 always sends users back to `https://<your-tenant>.auth0.com/login/callback` after a social login, and the sample app's authorize endpoint strict-compares against this value. 

Let’s grab the tenant domain with the Auth0 CLI.

First, make sure you are logged in to the CLI. The default `auth0 login` token does not include the connection-management and client grant scopes we will need later for using token vault and connected accounts, so log in once with the full set of necessary scopes now:

```bash
auth0 login --scopes "create:connections,update:connections,read:connections,delete:connections,create:client_grants"
```

Then list your tenants:

```bash
auth0 tenants list
```

Look for the row marked `→` (your currently active tenant) and plug the domain into `.env.local`:

```bash
# .env.local
OAUTH2_REDIRECT_URI=https://<your-tenant>.auth0.com/login/callback
```

Start the app:

```bash
bun dev   # or npm run dev
```

You should see:

```bash
Seeded 3 users and 12 tickets.
acme-helpdesk listening on http://localhost:3001
```

Confirm it is alive:

```bash
curl -sSf http://localhost:3001/health
# {"status":"ok","service":"acme-helpdesk",...}
```
<div class="alert alert-info alert-icon">
  <i class="icon-budicon-487 icon-info"></i>
  <b>Demo accounts: three seeded users alice@acme.test, bob@acme.test, carol@acme.test share the password "demo1234." They are documented in the sample's README.md.</b><br> 
</div>

Now make the app publicly accessible so Auth0 can reach it. In a second terminal:

```bash
ngrok http 3001
# or: cloudflared tunnel --url http://localhost:3001
```

`ngrok` prints a line like `https://<slug>.ngrok-free.app`; `cloudflared` prints a `https://<slug>.trycloudflare.com` equivalent. Copy that URL. We will call it `$ACME_HOST` for the rest of the post.

Excellent. Our "internal API" is now reachable from the public internet.

### Checkpoint

At this point, you should be able to:


- cURL `https://$ACME_HOST/health` and get a JSON response
- Open `https://$ACME_HOST/authorize?client_id=auth0-custom-oauth2&redirect_uri=<your-auth0-callback>&response_type=code&scope=openid+profile+read:tickets+write:tickets&state=x` in a browser and see the sample app's login form render. (The flow will not complete past login yet. We will register the Auth0 connection in the next section.)

## Registering the Custom OAuth2 Connection in Auth0

Now the fun part. We will tell Auth0 about our service and flip the Token Vault switch on the connection. Enabling the service on the `auth0-tv` application and setting up the rest of the Token Vault plumbing (MRRT, client grants, My Account API) is handled by `auth0-tv init` in the next section, so we do not need to touch that here.

### Create the connection via the Auth0 CLI

Create the variables below so your shell can interpolate them into the payload in the next command. Make sure **not** to include `https://` in the host URL.

```bash
export ACME_HOST=<your-ngrok-hostname>          # e.g. c021-165-85-243-52.ngrok-free.app (no https://)
export OAUTH2_CLIENT_SECRET=<from .env.local>   # the 32-byte hex secret you generated earlier
```

Create the `fetchUserProfile` script. This is the Node.js function that Auth0 runs server-side to turn the [access token](https://auth0.com/docs/secure/tokens/access-tokens) into a user profile. Our sample's `/userinfo` endpoint is OIDC-shaped, so the script is minimal:

```bash
cat > /tmp/fetchUserProfile.js <<JS
function fetchUserProfile(accessToken, ctx, cb) {
 request.get(
   {
     url: 'https://$ACME_HOST/userinfo',
     headers: { Authorization: 'Bearer ' + accessToken },
     json: true,
   },
   function (err, _res, body) {
     if (err) return cb(err);
     cb(null, {
       user_id: body.user_id,
       email: body.email,
       name: body.name,
     });
   }
 );
}
JS
```

The only non-negotiable field in the returned profile is `user_id`, which Auth0 uses to key the connected account to the user. `email` and `name` are optional but helpful. The here-doc delimiter (`JS`) is unquoted on purpose, so bash expands `$ACME_HOST` as it writes the file.

Now compose the full connection payload. `jq` handles JSON-escaping the script and interpolating the host and secret:

```bash
jq -n \
 --rawfile script /tmp/fetchUserProfile.js \
 --arg host "$ACME_HOST" \
 --arg secret "$OAUTH2_CLIENT_SECRET" \
 '{
   name: "acme-helpdesk",
   strategy: "oauth2",
   connected_accounts: { active: true },
   options: {
     client_id: "auth0-custom-oauth2",
     client_secret: $secret,
     authorizationURL: "https://\($host)/authorize",
     tokenURL: "https://\($host)/token",
     scope: "openid profile read:tickets write:tickets",
     scripts: { fetchUserProfile: $script }
   }
 }' > /tmp/acme-connection.json
```

Then create the connection:

```bash
auth0 api post connections --data "$(cat /tmp/acme-connection.json)" \
 | jq '{id, name, strategy, connected_accounts}'
```

You should see:

```json
{
 "id": "con_xxxxxxxxxxxxxxxx",
 "name": "acme-helpdesk",
 "strategy": "oauth2",
 "connected_accounts": { "active": true }
}
```

That one call covers both the connection creation and the "Connected Accounts for Token Vault" toggle, thanks to `connected_accounts.active: true` in the payload.

The connection name (`acme-helpdesk`) is the slug you will use with `auth0-tv connect` later. Pick a connection name to match your own service when you do this for real.

<div class="alert alert-info alert-icon">
  <i class="icon-budicon-487 icon-info"></i>
  <b>**Note**: Connection names can only contain letters, digits, and dashes.</b><br> 
</div>

### Checkpoint

At this point, you should be able to:

- Run `auth0 api get "connections?name=acme-helpdesk" | jq '.[0].connected_accounts'` and see `{"active": true}`

The connection exists on your tenant but is not yet enabled on the `auth0-tv` application. `auth0-tv init` takes care of that in the next section.

### Alternative: configure via the Auth0 Dashboard

Prefer the UI? The sample repo's [README has the full Dashboard walkthrough](https://github.com/deepu105/acme-helpdesk-oauth2-sample#register-via-the-auth0-dashboard): three click-throughs in **Authentication → Social** to create the connection, flip the Connected Accounts for Token Vault toggle, and (optionally) enable the connection on your `auth0-tv` app.

The Auth0 side is done. Everything from here will be in the terminal.

## Wiring `auth0-tv` to the New Connection

`auth0-tv` is a small open-source CLI that wraps Auth0's Token Vault flows (`login`, per-service `connect`, and authenticated `fetch`) for use by humans and AI agents. Install it globally:

```bash
bun add -g auth0-token-vault-cli   # or npm install -g auth0-token-vault-cli
```

<div class="alert alert-info alert-icon">
  <i class="icon-budicon-487 icon-info"></i>
  <b>bunx auth0-token-vault-cli works for humans but not for agents running the [Agent Skill](https://auth0.com/blog/what-ai-tools-mcp-servers-and-skills-actually-do/) shipped with the CLI.</b><br> 
</div>

Run the interactive `auth0-tv init` wizard. It bootstraps the Auth0 side (tenant configuration, Token Vault grant type, MRRT, callback URLs) and registers your connections:

```bash
auth0-tv init
```

Follow the prompts. When `init` asks how to configure the application, pick **[Create a new application](https://auth0.com/docs/get-started/auth0-overview/create-applications)** (or **Use an existing application** if you already have one set up for Token Vault). When it asks which connections to enable, make sure `acme-helpdesk` is selected.

This opens a browser, signs you in using the new Auth0 app that was just created, and returns control to the terminal. You can either create a new account with an email and password using the **Sign up** link when prompted to sign in, or sign in using the **Continue with Acme-helpdesk** button and its seeded users like `carol@acme.test` with password `demo1234`.

Now connect the new custom OAuth2 connection, using the `acme-helpdesk` slug you chose earlier:

```bash
auth0-tv connect acme-helpdesk \
 --scopes "openid,profile,read:tickets,write:tickets" \
 --allowed-domains "$ACME_HOST"
```

What happens:

1. `auth0-tv` redirects you through Auth0's Connected Accounts flow.
2. Auth0 redirects you to our sample app's `/authorize` endpoint.
3. You sign in as one of the demo users (for example, `alice@acme.test` / `demo1234`) and click **Allow** on the consent screen.
4. Auth0 stores the resulting access + refresh tokens in Token Vault, under your Auth0 user profile.

Back in the terminal, confirm the connection:

```bash
auth0-tv connections
```

You should see `acme-helpdesk` listed with the scopes you requested.

Now make your first authenticated call:

```bash
auth0-tv fetch acme-helpdesk https://$ACME_HOST/tickets
```

The CLI pulls a fresh access token from Token Vault (exchanging your refresh token under the hood), calls the URL with `Authorization: Bearer ...` injected, and prints the JSON response. Neither you nor the agent ever saw a token in plaintext.

That is it. You have set up a custom OAuth2 connection for Token Vault.

### Checkpoint

At this point, you should be able to:

- See `acme-helpdesk` under `auth0-tv connections`
- List tickets via `auth0-tv fetch acme-helpdesk https://$ACME_HOST/tickets`

## Using It from Claude Code

This is the part that turns a clever CLI into something an agent can use.

`auth0-token-vault-cli` ships an [Agent Skill](https://agentskills.io/) that teaches supported agents how to call `auth0-tv` correctly: which scopes map to which services, how to interpret errors, and how to re-run `connect` when a scope is missing. The skill is available through the Claude Code plugin marketplace.

Install it:

```bash
/plugin marketplace add deepu105/auth0-token-vault-cli
/plugin install auth0-token-vault@auth0-token-vault
/reload-plugins
```

Now, try asking the assistant: 

**You:** "/auth0-token-vault Show me the open tickets assigned to me in acme-helpdesk."

Claude Code picks up the `auth0-token-vault` skill and resolves that `acme-helpdesk` is a connected service. You will get the ticket list back, but you might notice the agent spends a few back-and-forth calls figuring out the API shape first: a GET on `/tickets/me`, then `/api/tickets`, then `/tickets?user=me`, before landing on `/tickets?assignee=me&status=open`. The `auth0-token-vault` skill knows how to talk to Token Vault but has no idea what endpoints `acme-helpdesk` exposes.

The fix is a second skill sitting on top: a small `acme-helpdesk` skill that documents the API so the agent does not have to guess. The sample repo ships one at [`skills/acme-helpdesk/SKILL.md`](https://github.com/deepu105/acme-helpdesk-oauth2-sample/blob/main/skills/acme-helpdesk/SKILL.md). It is ~70 lines of markdown: a short API reference (endpoints, filters, value constraints), a couple of example commands, and a note about `auth0-tv`'s `--json` response envelope. No code.

Drop it into your Claude Code user skills directory:

```bash
# Point this at your local clone of the sample repo                                                 
export ACME_SAMPLE_DIR=~/path/to/acme-helpdesk-oauth2-sample 
mkdir -p ~/.claude/skills/acme-helpdesk
cp "$ACME_SAMPLE_DIR/skills/acme-helpdesk/SKILL.md" \
 ~/.claude/skills/acme-helpdesk/SKILL.md
```

Reload plugins (`/reload-plugins`), then try the same prompt again: 

**You:** "/acme-helpdesk Show me the open tickets assigned to me."

This time, Claude goes straight to `auth0-tv --json fetch acme-helpdesk "https://$ACME_HOST/tickets?assignee=me&status=open"` and returns the list on the first try, no trial and error.

<picture>
<img src="https://images.ctfassets.net/23aumh6u8s0i/6G5nJ2aIOr016UF21tnvuh/86dd84a450dd0340600a01cc8efc3220/claude-tickets-list.png" alt="Claude Code listing open tickets assigned to the user" style="width:100%; margin: 1em auto; border: solid black 0px; border-radius: 0px;">
</picture>

A few more prompts worth trying:

**You:** "/acme-helpdesk Open a new P1 incident titled 'Checkout 500s spiking' in the payments team."

The agent should call `POST /tickets` with the right body and report back with the new ticket ID.

**You:** "/acme-helpdesk List all in-progress tickets on the platform team."

Filters combine cleanly. The agent picks the right query string.

**You:** "/acme-helpdesk Mark ticket 42 as resolved."

A `PATCH /tickets/42` with `{"status":"resolved"}`. The agent will echo the updated record.

Great! Your coding assistant can now drive an API it has never seen before, using the same credentials flow as a human user, with no agent-level token handling.

### Using it from OpenClaw

[OpenClaw](https://openclaw.ai) users get the same skill through [ClawHub](https://clawhub.ai):

```bash
npx clawhub@latest install auth0-token-vault
```

Restart OpenClaw, and the same prompts work. The skill manifest is shared, so the behavior is identical. You can also add the same `acme-helpdesk` skill to OpenClaw by dropping it into the user skills directory:

```bash
mkdir -p ~/.openclaw/skills/acme-helpdesk
cp ~/path/to/acme-helpdesk-oauth2-sample/skills/acme-helpdesk/SKILL.md \
 ~/.openclaw/skills/acme-helpdesk/SKILL.md
```

Restart OpenClaw once more, and the agent gets the same one-shot behavior: no endpoint guessing, direct `auth0-tv --json fetch` on the right URL.

## What You Have Accomplished

- Ran a small OAuth2-compliant API locally and made it publicly accessible for Auth0.
- Registered the API as an **Auth0 custom OAuth2 social connection** with **Connected Accounts for Token Vault** enabled.
- Connected to it from the terminal via `auth0-tv connect acme-helpdesk`, and called it with `auth0-tv fetch`.
- Gave **Claude Code** (and, optionally, OpenClaw) the same capability via an Agent Skill, without your agent ever handling a token.

### Extending further

- **Swap the helpdesk for your own service.** The steps are identical. Change the authorization/token URLs, point the fetch-user-profile script at your `/userinfo` endpoint, pick your own scopes, and you are in.
- **Add more scopes.** `auth0-tv connect <name> --scopes ...` accepts any scope string your service understands. Connecting again with a broader scope prompts a re-consent.
- **Mix with built-in connections.** The same [Connected Accounts](https://auth0.com/docs/secure/tokens/token-vault/connected-accounts-for-token-vault) machinery links a user profile to Google, GitHub, Slack, _and_ your internal helpdesk simultaneously. The agent sees them all through one CLI.

## Learn More About Auth0 Token Vault

[Auth0 Token Vault](https://auth0.com/ai/docs/intro/token-vault) lets your AI agent call external APIs on a user's behalf without ever seeing the user's credentials, whether those APIs come from a built-in integration like Google or Slack, or a first-party internal service wired in through a custom OAuth2 connection like we did here. That combination, a single agent-visible surface over both the public SaaS world and your own APIs, is what [Auth0 for AI Agents](https://a0.to/ai-content) is built for.

Sign up for [Auth0 for AI Agents](https://a0.to/ai-content).