Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ The following examples can be found in the examples directory:
- [Fastify API Example](./examples/example-fastify-api/README.md)
- [NestJS API Example](./examples/example-nestjs-api/README.md)
- [Express Web App Example](./examples/example-express-web/README.md)
- [FastMCP MCP Example](./examples/example-fastmcp-mcp/README.md)

Before running the examples, you need to install the dependencies for the monorepo and build all the packages.

Expand Down
4 changes: 4 additions & 0 deletions examples/example-fastmcp-mcp/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
AUTH0_DOMAIN=
AUTH0_AUDIENCE=
PORT=3001
MCP_SERVER_URL=http://localhost:3001
1 change: 1 addition & 0 deletions examples/example-fastmcp-mcp/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
registry=https://registry.npmjs.org
257 changes: 257 additions & 0 deletions examples/example-fastmcp-mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
# Example FastMCP MCP Server with Auth0 Integration

This is a practical example of securing a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/docs) server
with Auth0 authentication using the [FastMCP](https://github.com/punkpeye/fastmcp) TypeScript framework. It demonstrates
Comment thread
frederikprijck marked this conversation as resolved.
Outdated
real-world OAuth 2.0 and OIDC integration with JWT token verification and scope enforcement.

## Install dependencies

Install the dependencies using npm:

```bash
npm install
```

## Configuration

Rename `.env.example` to `.env` and configure the domain and audience:

```ts
AUTH0_DOMAIN = YOUR_AUTH0_DOMAIN;
AUTH0_AUDIENCE = YOUR_AUTH0_AUDIENCE;
```

With the configuration in place, the example can be started by running:

```bash
npm run start
```

## Testing

Use an MCP client like [MCP Inspector](https://github.com/modelcontextprotocol/inspector) to test your server interactively:

```bash
npx @modelcontextprotocol/inspector
```

The server will start up and the UI will be accessible at http://localhost:6274.

In the MCP Inspector, select `Streamable HTTP` as the `Transport Type` and enter `http://localhost:3001/mcp` as the URL.

## Auth0 Tenant Setup
Comment thread
patrickkang marked this conversation as resolved.

This guide uses [Auth0 CLI](https://auth0.github.io/auth0-cli/) to configure an Auth0 tenant for secure MCP tool access.

### Step 1: Authenticate with Auth0 CLI

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add installing the Auth0 CLI as a dependency? Or is this example usable without installing it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can configure your tenant from the dashboard or with the Management API. The Auth0 CLI isn't required, but it simplifies the setup process.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the entire next steps rely on the auth0 CLI, which makes it a required prerequisite to follow any of the following steps.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 That makes sense. I updated it to explicitly say that auth0 cli is a prerequisite to follow the rest of steps

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps, we can add a small sentence that explains everything can be configured through the manage dashboard as well if prefered.


First, you need to log in to the Auth0 CLI with the correct permissions (scopes) to manage all the necessary resources.
Comment thread
frederikprijck marked this conversation as resolved.
Outdated

1. Run the login command: This command will open a browser window for you to authenticate. We are requesting a set of
permissions to configure APIs, roles, clients, and actions.
Comment thread
frederikprijck marked this conversation as resolved.
Outdated

```
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,read:actions,create:actions,update:actions,delete:actions,update:triggers"
```

2. Verify your tenant: After logging in, confirm you are operating on the correct tenant.
Comment thread
frederikprijck marked this conversation as resolved.
Outdated

```
auth0 tenants list
```

### Step 2: Configure Tenant Settings

Next, enable tenant-level flags required for Dynamic Client Registration (DCR) and an improved user consent experience.

- `enable_dynamic_client_registration`: Allows MCP tools to register themselves as applications automatically.
[Learn more](https://auth0.com/docs/get-started/applications/dynamic-client-registration#enable-dynamic-client-registration)
- `enable_client_connections`: Allows dynamically registered clients to use domain-level connections.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have some "Learn more" for this as well?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I didnt find any reference to it in the public doc or blog posts

- `use_scope_descriptions_for_consent`: Shows user-friendly descriptions for permissions on the consent screen.
Comment thread
frederikprijck marked this conversation as resolved.
Outdated
[Learn more](https://auth0.com/docs/customize/login-pages/customize-consent-prompts).

Execute the following command to update the settings:
Comment thread
frederikprijck marked this conversation as resolved.
Outdated

```
auth0 api patch tenants/settings --data '{"flags": {"enable_dynamic_client_registration": true, "enable_client_connections": true, "use_scope_descriptions_for_consent": true}}'
```

### Step 3: Promote Connections to Domain Level

[Learn more](https://auth0.com/docs/authenticate/identity-providers/promote-connections-to-domain-level) about promoting
connections to domain level.

1. List your connections to get their IDs: `auth0 api get connections`
2. For each connection ID from the list, 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}'
```

### Step 4: Configure the API and Default Audience

This step creates the API (also known as a Resource Server) that represents your protected MCP tools and sets it as the
Comment thread
patrickkang marked this conversation as resolved.
Outdated
default for your tenant.

1. Create the API: This command registers the API with Auth0, defines its signing algorithm, enables Role-Based Access
Control (RBAC), and specifies the available permissions (scopes). Replace `http://localhost:3001` and `MCP Tools API`
Comment thread
patrickkang marked this conversation as resolved.
Outdated
with your desired identifier and name. Add your tool-specific scopes to the scopes array.

```
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"}
]
}'

```

2. Set the Default Audience: This ensures that users logging in interactively get access tokens that are valid for your
newly created MCP API. Replace `http://localhost:3001` with the same API identifier you used above.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why we do this in the context of MCP? Isn't this something that would work for any audience in any context? This just ensures that, when no audience is send along with authorize, this audience is used. Setting this as a default audience has consequences on existing clients and auth flows without audience.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are resorting to using default audience until we get the resource parameter support out.

More details here:
https://auth0.slack.com/archives/C091X0B50MA/p1753471387446879?thread_ts=1753459734.480379&cid=C091X0B50MA
and
https://oktawiki.atlassian.net/wiki/spaces/AFG1/pages/3666641175/RFD+resource+parameter?focusedCommentId=3669787109

We can explicitly call out that setting the default audience is a tenant-wide config change and that it affects all auth flows. WDYT?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might make sense to add a comment to this paragraph that indicates that this step is temporary until we have proper support for Resource Indicators for OAuth 2.0 as defined in RFC 8707. It is not an optional step, because if you skip it today, the issued AT will not be scoped specifically to the RS.
Good news is that support for RFC 8707 is coming very soon and we can tweak the instructions here to instead set the resource_parameter_profile to "compatibility".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed in 5643498

Comment thread
patrickkang marked this conversation as resolved.
Outdated

```
auth0 api patch "tenants/settings" --data '{"default_audience": "http://localhost:3001"}'
```

### Step 5: Configure RBAC Roles and Permissions

Now, set up roles and assign permissions to them. This allows you to control which users can access which tools.

1. Create Roles: For each role you need (e.g., "Tool Administrator", "Tool User"), run the create command.

```
# 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"
```

2. Assign Permissions to Roles: Get the ID of the role you just created (`auth0 roles list`) and assign the API
Comment thread
frederikprijck marked this conversation as resolved.
Outdated
permissions to it. Replace `YOUR_ROLE_ID`, `http://localhost:3001`, and the list of scopes.

```
# 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"
```

3. Assign Roles to Users: Find users and assign them to the roles.

```
# Find a user's ID
auth0 users search --query "email:\"example@google.com\""

# Assign the role using the user's ID and the role's ID
auth0 users roles assign "auth0|USER_ID_HERE" --roles "YOUR_ROLE_ID_HERE"
```

### Step 6: Configure and Deploy the Scope-Injection Action

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this section absolutely needed? I just went ahead and skipped it and my AT contains the scope claim with the right value (derived from the RBAC roles + permissions defined in Step 5).
Can we double-check that this is indeed needed or perhaps isn't needed anymore?

We could leave a comment about the fact that any further customization not supported out of the box by RBAC can be done via a custom Post-Login action trigger, but I'd consider that optional.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, we are removing the post-login action from the tenant settings and keep the tenant setup simple for now.

I've added this note for optional configuration: **Note:** Further customization not supported out of the box by RBAC can be done via a custom Post-Login action trigger.


This Auth0 Action will automatically add the correct scope claim to a user's access token based on the permissions
granted by their assigned roles.

1. Create the Action Code: Save the following JavaScript code to a file named `mcp-action.js`. Replace
`http://localhost:3001` with your API identifier and update the `ROLE_SCOPES_MAPPING` with your specific roles and
their associated scopes.

```
/**
* Auth0 Action: Inject MCP Scopes
*
* This Action injects MCP tool scopes into access tokens based on user roles.
* Only runs for tokens issued to the MCP API audience.
*
* This Action bridges Auth0's RBAC system with OAuth 2.0 scopes for
* interactive authentication flows. It ensures that users with MCP
* roles receive tokens with appropriate tool access permissions.
*
*/
exports.onExecutePostLogin = async (event, api) => {
// IMPORTANT: Verify the configuration values below match your setup from the previous steps
const targetAudience = 'http://localhost:3001';
const roleToScopesMapping = {"Tool User":["tool:whoami"],"Tool Administrator":["tool:whoami","tool:greet"]};

// Debug configuration - enabled only when the secret is set to 'true'
const DEBUG_ENABLED = event.secrets?.DEBUG_ENABLED === 'true';

// Safe debug logger that avoids logging secrets
const debug = (message) => {
if (DEBUG_ENABLED) {
console.log(message);
}
};

const actualAudience = event.resource_server?.identifier;

// Debug: Log initial state - carefully avoiding sensitive data
debug(' --------- MCP SCOPES ACTION START ---------');
debug(` Target audience: ${targetAudience}`);
debug(` Actual audience: ${actualAudience || 'undefined'}`);
debug(` Available roles in mapping: ${Object.keys(roleToScopesMapping).join(', ')}`);

const roles = event.authorization?.roles || [];
debug(` User has ${roles.length} roles`);

// Only process tokens intended for the MCP API
if (actualAudience === targetAudience) {
debug(' AUDIENCE MATCHED! Processing scopes...');

if (roles.length === 0) {
debug(' WARNING: User has no roles assigned');
}

let addedScopesCount = 0;

// Iterate through user roles and add corresponding scopes
for (const roleName of roles) {
debug(` Processing role #${roles.indexOf(roleName) + 1}/${roles.length}`);

if (roleToScopesMapping[roleName]) {
const scopes = roleToScopesMapping[roleName];
debug(` Found ${scopes.length} scopes for this role`);

for (const scope of scopes) {
debug(` Adding scope #${scopes.indexOf(scope) + 1}`);
api.accessToken.addScope(scope);
addedScopesCount++;
}
} else {
debug(` No scopes defined for this role`);
}
}

debug(`SUCCESS: Added ${addedScopesCount} scopes to the token`);
} else {
debug('AUDIENCE MISMATCH! Skipping scope injection.');
debug(`Expected "${targetAudience}" but got "${actualAudience || 'undefined'}"`);
}

debug('--------- MCP SCOPES ACTION COMPLETE ---------');
};
```

2. Create and Deploy the Action: Use the CLI to create the action from your file and deploy it.

```
# Create the action (this will output the action's ID)
auth0 actions create --name "Inject MCP Scopes" --trigger post-login --code "$(cat mcp-actions.js)" --no-input --json

# Deploy the action using its ID
auth0 actions deploy YOUR_ACTION_ID_HERE
```

3. Bind the Action: Attach the deployed action to the "Post Login" trigger so it runs every time a user logs in.

```
auth0 api patch "actions/triggers/post-login/bindings" --data '{"bindings": [{"ref": {"type": "action_id", "value": "YOUR_ACTION_ID_HERE"}, "display_name": "Inject MCP Scopes"}]}'
```
24 changes: 24 additions & 0 deletions examples/example-fastmcp-mcp/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "example-fastmcp-mcp",
"version": "0.1.0-beta.1",
"description": "FastMCP Server Example (Beta) — Example implementation of an MCP server using FastMCP framework with Auth0 authentication. This example is in beta and demonstrates secure MCP server patterns with OAuth 2.0 and JWT validation.",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the approach to move this out of beta post merging this example?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call out. We are starting with beta and might not support every framework. The idea is to first validate those mcp examples and get some feedback. Framework popularity with mcp server integration is also a factor. Curious if there's an existing path we usually follow to move examples here out of beta

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No we have never realy used a beta concept with examples, as they are not released like versioned artifacts.

"type": "module",
"scripts": {
"start": "tsx src/index.ts --project tsconfig.json"
},
"dependencies": {
"@auth0/auth0-api-js": "*",
"@modelcontextprotocol/sdk": "^1.17.2",
"dotenv": "^17.2.1",
"fastmcp": "^3.12.0",
"zod": "^4.0.17"
},
"devDependencies": {
"tsx": "^4.20.3",
"typescript": "^5.8.3"
},
"keywords": [
"fastmcp",
"example"
]
}
Loading