Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
146 changes: 26 additions & 120 deletions examples/example-fastmcp-mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,33 +12,6 @@ Install the dependencies using npm:
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.

### Pre-requisites:
Expand All @@ -50,10 +23,10 @@ This guide uses [Auth0 CLI](https://auth0.github.io/auth0-cli/) to configure an
First, you need to log in to the Auth0 CLI with the correct scopes to manage all the necessary resources.

1. Run the login command: This command will open a browser window for you to authenticate. We are requesting a set of
scopes to configure APIs, roles, clients, and actions.
scopes to configure APIs, roles, and clients.

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

2. Verify your tenant: After logging in, confirm you are operating on the tenant you want to configure.
Expand All @@ -74,7 +47,7 @@ Next, enable tenant-level flags required for Dynamic Client Registration (DCR) a
Execute the following command to enable the above mentioned flags through the tenant settings:

```
auth0 api patch tenants/settings --data '{"flags": {"enable_dynamic_client_registration": true, "use_scope_descriptions_for_consent": true}}'
auth0 tenant-settings update set flags.enable_dynamic_client_registration flags.use_scope_descriptions_for_consent
```

### Step 3: Promote Connections to Domain Level
Expand All @@ -91,7 +64,7 @@ auth0 api patch connections/YOUR_CONNECTION_ID --data '{"is_domain_connection":

### 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
This step creates the API (also known as a Resource Server) that represents your protected MCP Server and sets it as the
default for your tenant.

1. Create the API: This command registers the API with Auth0, defines its signing algorithm, enables Role-Based Access
Expand All @@ -116,7 +89,9 @@ auth0 api post resource-servers --data '{
```

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.
newly created MCP Server. Replace `http://localhost:3001` with the same API identifier you used above.

**Note:** This step is currently required but temporary. Without setting a default audience, the issued access tokens will not be scoped specifically to your MCP resource server. Support for RFC 8707 (Resource Indicators for OAuth 2.0) is coming soon, which will provide proper resource targeting. Once available, these instructions will be updated to use `resource_parameter_profile: "compatibility"` instead of the default audience approach.
Comment thread
patrickkang marked this conversation as resolved.
Outdated

```
auth0 api patch "tenants/settings" --data '{"default_audience": "http://localhost:3001"}'
Expand Down Expand Up @@ -157,103 +132,34 @@ auth0 users search --query "email:\"example@google.com\""
auth0 users roles assign "auth0|USER_ID_HERE" --roles "YOUR_ROLE_ID_HERE"
```

### Step 6: Configure and Deploy the Scope-Injection Action
**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.
## Configuration

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.
Rename `.env.example` to `.env` and configure the domain and audience:

```
/**
* 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 ---------');
};
# Auth0 tenant domain
AUTH0_DOMAIN=example-tenant.us.auth0.com

# Auth0 API Identifier
AUTH0_AUDIENCE=http://localhost:3001
```

2. Create and Deploy the Action: Use the CLI to create the action from your file and deploy it.
With the configuration in place, the example can be started by running:

```bash
npm run start
```
# 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
```
## Testing

3. Bind the Action: Attach the deployed action to the "Post Login" trigger so it runs every time a user logs in.
Use an MCP client like [MCP Inspector](https://github.com/modelcontextprotocol/inspector) to test your server interactively:

```bash
npx @modelcontextprotocol/inspector
```
auth0 api patch "actions/triggers/post-login/bindings" --data '{"bindings": [{"ref": {"type": "action_id", "value": "YOUR_ACTION_ID_HERE"}, "display_name": "Inject MCP Scopes"}]}'
```

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.
1 change: 0 additions & 1 deletion examples/example-fastmcp-mcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"@modelcontextprotocol/sdk": "^1.17.2",
"dotenv": "^17.2.1",
"fastmcp": "^3.12.0",
"jose": "^6.0.12",
"zod": "^4.0.17"
},
"devDependencies": {
Expand Down
50 changes: 23 additions & 27 deletions examples/example-fastmcp-mcp/src/auth0.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import {
InvalidTokenError,
} from "@modelcontextprotocol/sdk/server/auth/errors.js";
import { getOAuthProtectedResourceMetadataUrl } from "@modelcontextprotocol/sdk/server/auth/router.js";
import { AccessTokenClaims, FastMCPAuthSession } from "./types.js";
import { MCP_TOOL_SCOPES } from "./tools.js";
import { FastMCPAuthSession } from "./types.js";

const PORT = parseInt(process.env.PORT ?? "3001", 10);
const MCP_SERVER_URL = process.env.MCP_SERVER_URL ?? `http://localhost:${PORT}`;
Expand All @@ -18,18 +17,8 @@ const apiClient = new ApiClient({
audience: AUTH0_AUDIENCE,
});

function validateScopes(
token: AccessTokenClaims,
requiredScopes: string[]
): boolean {
let tokenScopes: string[] = [];

if (token.scope) {
tokenScopes =
typeof token.scope === "string" ? token.scope.split(" ") : token.scope;
}

return requiredScopes.every((required) => tokenScopes.includes(required));
function isNonEmptyString(value: unknown): value is string {
return typeof value === "string" && value.length > 0;
}

export const authenticate = async (
Expand All @@ -45,21 +34,26 @@ export const authenticate = async (
if (type?.toLocaleLowerCase() !== "bearer" || !accessToken) {
throw new InvalidTokenError("Invalid authorization header");
}
const decoded = (await apiClient.verifyAccessToken({
const decoded = await apiClient.verifyAccessToken({
accessToken,
})) as AccessTokenClaims;
});

const clientId = decoded.client_id ?? decoded.azp;
if (!clientId) {
if (!isNonEmptyString(decoded.sub)) {
throw new InvalidTokenError(
"Token is missing required client identification (client_id or azp claim)."
"Token is missing required subject (sub) claim"
);
}

// Validate required scopes
if (!validateScopes(decoded, MCP_TOOL_SCOPES)) {
throw new InsufficientScopeError(
`Token is missing required scopes: ${MCP_TOOL_SCOPES.join(", ")}`
let clientId: string | null = null;
if (isNonEmptyString(decoded.client_id)) {
clientId = decoded.client_id;
} else if (isNonEmptyString(decoded.azp)) {
clientId = decoded.azp;
}

if (!clientId) {
throw new InvalidTokenError(
"Token is missing required client identification (client_id or azp claim)."
);
}

Expand All @@ -73,10 +67,12 @@ export const authenticate = async (
...(decoded.exp && { expiresAt: decoded.exp }),
extra: {
sub: decoded.sub,
...(decoded.client_id && { client_id: decoded.client_id }),
...(decoded.azp && { azp: decoded.azp }),
...(decoded.name && { name: decoded.name }),
...(decoded.email && { email: decoded.email }),
...(isNonEmptyString(decoded.client_id) && {
client_id: decoded.client_id,
}),
...(isNonEmptyString(decoded.azp) && { azp: decoded.azp }),
...(isNonEmptyString(decoded.name) && { name: decoded.name }),
...(isNonEmptyString(decoded.email) && { email: decoded.email }),
},
} satisfies FastMCPAuthSession;

Expand Down
10 changes: 2 additions & 8 deletions examples/example-fastmcp-mcp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import "dotenv/config";

import { FastMCP } from "fastmcp";
import { FastMCPAuthSession } from "./types.js";
import { tools, MCP_TOOL_SCOPES } from "./tools.js";
import { MCP_TOOL_SCOPES, registerTools } from "./tools.js";
import { authenticate } from "./auth0.js";

const PORT = parseInt(process.env.PORT ?? "3001", 10);
Expand Down Expand Up @@ -82,13 +82,7 @@ const start = async () => {
/**
* Registers all tools to the FastMCP server.
*/
for (const tool of tools) {
if (tool.parameters) {
server.addTool<typeof tool.parameters>(tool);
} else {
server.addTool(tool);
}
}
registerTools(server);

try {
/**
Expand Down
Loading