Skip to main content
To call your APIs on behalf of your users, the MCP server needs to exchange the Auth0 access token it received from the MCP client (with the audience set to the MCP server itself) for a new Auth0 access token with the audience set to your API. In Auth0, this is called Custom Token Exchange and uses RFC 8693. This flow involves the MCP server acting as both a resource server (for the client) and a client (for the upstream API). By the end of this quickstart, you should have an MCP server that can:
  • Exchange an Auth0 access token for another Auth0 access token with a different audience using Custom Token Exchange
  • Verify the access token using the @auth0/auth0-api-js library (which uses jose under the hood) against your tenant JWKS, enforcing issuer, audience, and RS256

Pre-requisites

Auth for MCP is currently available in Early Access. To join the Early Access program, please complete this form. We’ll reach out to you when your request is processed.
To continue with this quickstart, you need to have an Auth0 account.
To use the resource parameter in your access tokens, you need to enable the compatibility profile.The quickest way to enable it is through the Auth0 Dashboard:
  1. Navigate to Settings on the left sidebar.
  2. Click on Advanced on the top right corner.
  3. Scroll down to the Settings section, find and enable Resource Parameter Compatibility Profile.
Resource Parameter Compatibility Profile enabled
Resource Parameter Compatibility Profile is available under the Early Access program. Go to the Early Access section to learn how to gain access.
This guide uses Auth0 CLI to configure an Auth0 tenant for secure MCP tool access.
  1. Follow the Auth0 CLI installation instructions.
  2. Log in to your account with the Auth0 CLI:
auth0 login --scopes "read:client_grants,create:client_grants,delete:client_grants,read:clients,create:clients,update:clients,read:resource_servers,create:resource_servers,update:resource_servers,read:roles,create:roles,update:roles,update:tenant_settings,read:connections,update:connections"
Confirm you are in the correct tenant by double-checking on the terminal the tenant domain, or run the command below:
auth0 tenants list
If more than one tenant is configured, the default tenant will be indicated by an arrow.
To simplify the process of interacting with the Auth0 CLI, we recommend installing jq. This will allow you to easily parse JSON responses from the CLI.
  • MacOS
  • Linux
  • Windows
brew install jq

Configure tenant settings

To allow third-party clients like MCP inspector to use a connection like username-and-password or a social connection, you need to promote the connection to a domain level. Learn more about enabling third-party applications.
1

List your connection

List your connections to get their IDs
auth0 api get connections
2

Choose your connections

From the list, identify which connections you want to use for the MCP server and copy the ID.
  • For the username and password database, look for the connection with the strategy auth0.
  • For Google social connection, look for the connection with the strategy google-oauth2.
3

Upgrade the connection to domain-level

For each of those specific connection IDs, run the following command to mark it as a domain-level connection. Replace YOUR_CONNECTION_ID with the actual ID (e.g., con_XXXXXXXXXXXXXXXX)
auth0 api patch connections/YOUR_CONNECTION_ID --data '{"is_domain_connection": true}'
The sample application in this quickstart is prepared to show different tools depending on the role of a given user. For example an Tool Administrator will have access to different tools from an Tool User.
If you decide to skip this step, you’ll only have access to the tool get_current_time, which is the only tool in the sample apps that doesn’t require any permissions.
If your application doesn’t require that level of access, i.e. all users have access to available tools, this step is not required for your use case.Let’s create roles that allow you to control which users can access which tools.

Create roles

For each role you need (e.g., “Tool Administrator”, “Tool User”), run the create command as below.
# Example for an admin role
auth0 roles create --name "Tool Administrator" --description "Grants access to all MCP tools"

# Example for a basic user role
auth0 roles create --name "Tool User" --description "Grants access to basic MCP tools"
Save the ID from the output (they start with rol_) as you’ll need them for the next step.

Assign permissions to roles

After creating roles, assign the API permissions to it:
# Example for admin role (all scopes)
auth0 roles permissions add YOUR_ADMIN_ROLE_ID --api-id "http://localhost:3001/" --permissions "tool:whoami,tool:greet"

# Example for user role (one scope)
auth0 roles permissions add YOUR_USER_ROLE_ID --api-id "http://localhost:3001/" --permissions "tool:whoami"

Assign roles to users

Find users and assign them to the roles. You can search by their email address:
auth0 users search --query "email:\"example@google.com\""
Copy the user ID (USERID) from last command’s output then assign the role to the user:
auth0 users roles assign "USER_ID_HERE" --roles "YOUR_ROLE_ID_HERE"

Set up the Auth0 Applications and APIs

When an MCP server needs to call underlying APIs, it needs to perform a Custom Token Exchange to obtain an access token with the audience set to the API rather than the MCP server itself. Because of this architecture, the MCP server acts in a dual role.
  1. To the MCP client (e.g., an IDE, an AI assistant), the MCP server acts as a Resource Server (an API).
  2. To the underlying API (e.g., your own API), the MCP server acts as a Client.
Because of this duality, you need to set up the MCP server twich on your Auth0 tenant, both as an API and as a Client, as described below.

Create an API to represent your MCP server

An MCP server is treated just like any other API in your Auth0 tenant, which means you can use the same access control, scoping, and authorization features. The API (also known as a Resource Server) represents your protected MCP Server. Run the following command to create an API in Auth0:
auth0 api post resource-servers --data '{
  "identifier": "http://localhost:3001/",
  "name": "MCP Tools API",
  "signing_alg": "RS256",
  "token_dialect": "rfc9068_profile_authz",
  "enforce_policies": true,
  "scopes": [
    {"value": "tool:whoami", "description": "Access the WhoAmI tool"},
    {"value": "tool:greet", "description": "Access the Greeting tool"}
  ]
}'
Note that rfc9068_profile_authz is used as the token dialect to include the permissions claim in the access token, which can be useful for the API to check user permissions without making additional calls. For more details on protecting APIs with Auth0 check our quickstarts.

Create an Application for your MCP server

In the custom token exchange scenario, the MCP server acts as a client in order to obtain an Auth0 access token with custom token exchange:
auth0 api post clients --data '{
  "name": "MCP Server Client",
  "app_type": "non_interactive",
  "is_first_party": true,
  "grant_types": ["authorization_code"],
  "token_endpoint_auth_method": "client_secret_post",
  "oidc_conformant": true,
  "jwt_configuration": { "alg": "RS256" }
}' | jq -r '"\nClient ID:     " + .client_id + "\nClient Secret: " +  .client_secret'
Save the Client ID and the Client Secret from the output. Now use the Client ID from the last command and allow your client to use custom token exchange:
auth0 api patch clients/<client_id> --data '{
  "token_exchange": {
    "allow_any_profile_of_type": ["custom_authentication"]
  }
}'
And finally, because you need an API to call to, let’s create one.

Create a first-party API to call from the MCP server

Since your objective is to have the MCP server make a tool call that calls a protected first-party API you need to configure an API in Auth0, this will be your upstream API:
auth0 api post resource-servers --data '{
  "identifier": "http://localhost:8787/",
  "name": "Protected First Party API",
  "signing_alg": "RS256",
  "enforce_policies": true,
  "scopes": [
    {"value": "read:private", "description": "Private scope"}
  ]
}' | jq -r '"Audience: " + .identifier'
Save the Audience from the command output, you’ll need it on a later step. The sample app demonstrates custom token exchange with a greet tool that calls your protected API on behalf of the authenticated user.

Install packages

Ensure you have npm installed or follow the instructions to install npm in its documentation. In the fastmcp-mcp-customtokenexchange-js directory, install the required packages:
npm install

Create your environment file

In the fastmcp-mcp-customtokenexchange-js directory, rename the .env.example to .env file and update the following content:
# Auth0 tenant domain
AUTH0_DOMAIN=<your-tenant.auth0.com>
# Auth0 API Identifier
AUTH0_AUDIENCE=http://localhost:3001/

# Server port
PORT=3001

# MCP server URL
MCP_SERVER_URL=http://localhost:3001/

# MCP Auth0 client credentials and configuration
MCP_AUTH0_CLIENT_ID=your-client-id
MCP_AUTH0_CLIENT_SECRET=your-client-secret

# Subject token type for CTE
MCP_AUTH0_SUBJECT_TOKEN_TYPE=urn:fastmcp:mcp

# Scopes to request
MCP_AUTH0_EXCHANGE_SCOPE=read:tasks

# Upstream API configuration
API_AUTH0_AUDIENCE=http://localhost:8787/
API_BASE_URL=http://localhost:8787/
To get your Auth0 application’s AUTH0_DOMAIN, run the following command:
auth0 tenants list
Copy the domain under TENANT from the output and update the corresponding variable on the .env. For MCP_AUTH0_CLIENT_ID and MCP_AUTH0_CLIENT_SECRET you will use the values obtained on the Create an Application for your MCP server step.

Use Custom Token Exchange Action

This Action is the server-side logic Auth0 executes to perform the token exchange. It is necessary because the MCP server receives an access token from the client (with the MCP server as its audience) and must exchange it for a new token (with the upstream API as the audience). This Action validates the original token and mints the new one. The Custom Token Exchange Action, available as a part of Custom Token Exchange Early Access. Navigate to the On-behalf-of token exchange for first-party apps template available here and click on Use This Template.
Action On-behalf-of token exchange for first-party apps template page
This will open a modal for you to name the action:
Action creation modal
Once the action is created you can Deploy it. When you deploy the Action, Auth0 assigns it an Action ID. You still need to add your custom logic to the Action, but first, get the Action ID to create the Custom Token Exchange Profile.

Setup the token exchange profile

To enable Custom Token Exchange, you need to create a Token Exchange Profile that links your Custom Token Exchange Action to a unique subject token type identifier.
To create a token exchange profile, you need a Management API access token with the appropriate scopes.The quickest way to get a token for testing is from the Auth0 Dashboard:
  1. Navigate to Applications > APIs in your Auth0 Dashboard
  2. Select Auth0 Management API
  3. Click on the API Explorer tab
  4. Copy the displayed token
Management API Token in Dashboard
The token includes all the scopes granted to the API Explorer Application. For production environments, we recommend using the Client Credentials flow to acquire tokens with only the necessary scopes.
Save this token - you’ll use it in step 3.
You need the ID of the Custom Token Exchange Action you deployed earlier.Run the following command to list all actions in your tenant:
auth0 actions list
From the output, find the action that matches the name you used when creating your Custom Token Exchange Action. Copy the Action ID (it starts with act_).Example output:
ID                          NAME                                     TYPE
act_aBc123XyZ456            On-behalf-of token exchange             custom-token-exchange
Save this Action ID - you’ll use it in the next step.
Now use the Management API to create your Custom Token Exchange Profile.Replace the placeholder values in the command below:
  • YOUR_TENANT - Your Auth0 tenant domain (e.g., dev-abc123.us.auth0.com)
  • YOUR_MANAGEMENT_API_TOKEN - The token from Step 1
  • YOUR_PROFILE_NAME - A descriptive name for your profile (e.g., "MCP Custom Token Exchange")
  • <your-action-id> - The Action ID from Step 2
curl --location 'https://YOUR_TENANT/api/v2/token-exchange-profiles' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_MANAGEMENT_API_TOKEN' \
--data '{
    "name": "YOUR_PROFILE_NAME",
    "subject_token_type": "urn:fastmcp:mcp",
    "action_id": "<your-action-id>",
    "type": "custom_authentication"
}'
The subject_token_type must be a unique URI identifier. In this example, we use urn:fastmcp:mcp which is specific to FastMCP implementations. You cannot use reserved namespaces like urn:auth0.
A successful response will return the profile details including the profile ID and creation timestamp.For more information on managing token exchange profiles, see Custom Token Exchange documentation.

Run the MCP server and the API

Run this command to start your server:
npm run start
And in a separate window run this command to start the API:
npm run start:api

Exchange your access token

To call your APIs on behalf of your users, the MCP server needs to exchange the Auth0 access token it received from the MCP client (with the audience set to the MCP server itself) for a new Auth0 access token with the audience set to your API. In Auth0, this is called Custom Token Exchange and uses RFC 8693.

The Orchestrator: bearerForUpstream

The process begins with the bearerForUpstream function. Its main job is to take the initial token (the subjectToken), manage the exchange process, and handle any potential errors gracefully. This function serves as a safe wrapper around our exchange logic.
async function bearerForUpstream(subjectToken: string) {
  if (!subjectToken) return { token: null, scopes: null };

  try {
    const result = await exchangeCustomToken(subjectToken);
    return {
      token: result.accessToken,
      scopes: result.scope,
    }
  } catch (err) {
    console.error('Error during token exchange:', err);
    throw err;
  }
}
As you can see, it calls exchangeCustomToken and, on a successful exchange, returns the new accessToken and its associated scope. If the exchange fails, it logs the error and re-throws it to be handled upstream.

The core logic: exchangeCustomToken

This function, located in src/auth0.ts, contains the actual token exchange logic. It uses the ApiClient from the auth0-api-js SDK to simplify the interaction with Auth0’s /oauth/token endpoint. First, we initialize the ApiClient with the credentials of the application performing the exchange:
 const exchangeClient = new ApiClient({
  domain: AUTH0_DOMAIN,
  audience: API_AUTH0_AUDIENCE,
  clientId: MCP_AUTH0_CLIENT_ID,
  clientSecret: MCP_AUTH0_CLIENT_SECRET,
});
With the client configured, the exchangeCustomToken function calls the getTokenByExchangeProfile method. This method implements the Custom Token Exchange flow.
export async function exchangeCustomToken(subjectToken: string) {
    return await exchangeClient.getTokenByExchangeProfile(subjectToken, {
      subjectTokenType: MCP_AUTH0_SUBJECT_TOKEN_TYPE,
      audience: API_AUTH0_AUDIENCE,
      ...(MCP_AUTH0_EXCHANGE_SCOPE && { scope: MCP_AUTH0_EXCHANGE_SCOPE }),
    });
}

Testing your MCP server

Now that your MCP server is up and running, you can test it by using tools like MCP Inspector.

Learn how to test your Auth0 powered MCP server

Next steps