forked from auth0/auth0-auth-js
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathauth0.ts
More file actions
188 lines (169 loc) · 5.94 KB
/
Copy pathauth0.ts
File metadata and controls
188 lines (169 loc) · 5.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
import { ApiClient, VerifyAccessTokenError } from "@auth0/auth0-api-js";
import { discoverAuthorizationServerMetadata } from "@modelcontextprotocol/sdk/client/auth.js";
import { InvalidTokenError } from "@modelcontextprotocol/sdk/server/auth/errors.js";
import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
import {
getOAuthProtectedResourceMetadataUrl,
mcpAuthMetadataRouter,
} from "@modelcontextprotocol/sdk/server/auth/router.js";
import { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import type { RequestHandler } from "express";
import { ZodRawShape } from "zod";
import { Auth, Auth0McpOptions } from "./types.js";
import { MCP_TOOL_SCOPES } from "./tools.js";
export function createAuth0Mcp(opts: Auth0McpOptions) {
const verify = createVerifier(opts);
const requireScopes = createScopeValidator();
const authMetadataRouter = createAuthMetadataRouter(opts);
const authMiddleware = createAuthMiddleware(opts, verify);
return {
/**
* Creates an Express router that exposes OAuth metadata endpoints needed for MCP clients.
*/
authMetadataRouter: () => authMetadataRouter,
/**
* Creates Express middleware for protecting MCP endpoints.
* Validates Bearer tokens and populates req.auth with user information.
*/
authMiddleware: () => authMiddleware,
/**
* Wraps an MCP tool handler to enforce required OAuth scopes.
*/
requireScopes,
};
}
function isNonEmptyString(value: unknown): value is string {
return typeof value === "string" && value.length > 0;
}
/**
* Creates a JWT token verifier for Auth0-issued access tokens.
*
* This function returns a reusable `verify` function that validates JWT signatures,
* token claims, and extracts user identity information for MCP integration using
* the official @auth0/auth0-api-js library.
*/
function createVerifier(opts: Auth0McpOptions) {
const apiClient = new ApiClient({
domain: opts.domain,
audience: opts.audience,
});
return async function verify(token: string): Promise<Auth> {
try {
const decoded = await apiClient.verifyAccessToken({
accessToken: token,
});
if (!isNonEmptyString(decoded.sub)) {
throw new InvalidTokenError(
"Token is missing required subject (sub) claim"
);
}
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)."
);
}
return {
token,
clientId,
scopes:
typeof decoded.scope === "string"
? decoded.scope.split(" ").filter(Boolean)
: [],
...(decoded.exp && { expiresAt: decoded.exp }),
extra: {
sub: decoded.sub,
...(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 }),
},
};
} catch (error) {
if (error instanceof VerifyAccessTokenError) {
throw new InvalidTokenError(error.message);
}
throw error;
}
};
}
/**
* Returns a router that includes MCP authorization metadata endpoints.
*/
async function createAuthMetadataRouter(opts: Auth0McpOptions) {
const oauthMetadata = await discoverAuthorizationServerMetadata(
new URL(`https://${opts.domain}`)
);
if (!oauthMetadata) {
throw new Error(`Failed to fetch OAuth metadata from ${opts.domain}`);
}
return mcpAuthMetadataRouter({
oauthMetadata,
resourceServerUrl: opts.resourceServerUrl,
resourceName: opts.resourceName,
scopesSupported: ["openid", ...MCP_TOOL_SCOPES],
});
}
/**
* Returns an Express middleware that protects MCP endpoints.
* This middleware validates Bearer tokens, and checks for required scopes.
*/
function createAuthMiddleware(
opts: Auth0McpOptions,
verifier: (token: string) => Promise<Auth>
): RequestHandler {
return requireBearerAuth({
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(
opts.resourceServerUrl
),
verifier: { verifyAccessToken: verifier },
});
}
/**
* Wraps an MCP tool handler to enforce required OAuth scopes.
*
* This is a higher-order function that adds scope-based authorization to MCP tools.
* It validates that the authenticated user's JWT token contains all required scopes
* before allowing access to the wrapped tool.
*/
function createScopeValidator() {
/**
* Wraps a tool handler with scope validation.
* This function ensures that the tool can only be executed if the user has the required OAuth scopes.
*/
return function requireScopes<T extends ZodRawShape>(
requiredScopes: readonly string[],
handler: (args: T, extra: { authInfo: Auth }) => Promise<CallToolResult>
): ToolCallback<T> {
return (async (args, extra) => {
// To support both context-only and payload+context handlers
let context = extra;
if (!extra) {
context = args as Parameters<ToolCallback<T>>[1];
}
if (!context.authInfo) {
throw new Error(
"Authentication information is required to execute this tool."
);
}
const userScopes = context.authInfo.scopes;
const hasScopes = requiredScopes.every((scope) =>
userScopes.includes(scope)
);
if (!hasScopes) {
throw new Error(
`Missing required scopes: ${requiredScopes.join(", ")}`
);
}
return handler(args as T, { authInfo: context.authInfo as Auth });
}) as ToolCallback<T>;
};
}