Skip to content

Commit bfb1113

Browse files
authored
Merge pull request #6 from 3nethz/main
Fix mismatch with protocol
2 parents ac2101c + b9d32f5 commit bfb1113

File tree

6 files changed

+62
-34
lines changed

6 files changed

+62
-34
lines changed

.changeset/deep-wolves-cough.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@brionmario-experimental/mcp-express': patch
3+
'@brionmario-experimental/mcp-node': patch
4+
---
5+
6+
Fix endpoints to match the protocol, change error handling

examples/express-mcp-server/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ app.use(
1515
McpAuthServer({
1616
providers: [
1717
{
18-
baseUrl: process.env.BASE_URL || 'http://localhost:3000',
18+
baseUrl: process.env.BASE_URL as string,
1919
issuer: process.env.ISSUER,
2020
clientId: process.env.CLIENT_ID || '',
2121
clientSecret: process.env.CLIENT_SECRET,
@@ -31,7 +31,7 @@ app.use('/api', publicRoutes);
3131
app.use(
3232
'/api/protected',
3333
protectedRoute({
34-
baseUrl: process.env.BASE_URL || 'http://localhost:3000',
34+
baseUrl: process.env.BASE_URL as string,
3535
issuer: process.env.ISSUER,
3636
clientId: process.env.CLIENT_ID || '',
3737
clientSecret: process.env.CLIENT_SECRET,

packages/mcp-express/src/middlewares/protected-route.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,11 @@
1616
* under the License.
1717
*/
1818

19-
import {
20-
AUTHORIZATION_SERVER_METADATA_URL,
21-
validateAccessToken,
22-
McpAuthProvider,
23-
} from '@brionmario-experimental/mcp-node';
19+
import {PROTECTED_RESOURCE_URL, validateAccessToken, McpAuthProvider} from '@brionmario-experimental/mcp-node';
2420
import {NextFunction, Request, Response} from 'express';
2521

2622
export default function protectedRoute(provider?: McpAuthProvider) {
27-
return async function (
23+
return async function protectedMiddleware(
2824
req: Request,
2925
res: Response,
3026
next: NextFunction,
@@ -34,32 +30,39 @@ export default function protectedRoute(provider?: McpAuthProvider) {
3430
if (!authHeader) {
3531
res.setHeader(
3632
'WWW-Authenticate',
37-
`Bearer resource_metadata="${req.protocol}://${req.get('host')}${AUTHORIZATION_SERVER_METADATA_URL}"`,
33+
`Bearer resource_metadata="${req.protocol}://${req.get('host')}${PROTECTED_RESOURCE_URL}"`,
3834
);
3935
return res.status(401).json({
4036
error: 'unauthorized',
4137
error_description: 'Missing authorization token',
4238
});
4339
}
4440

45-
const parts = authHeader.split(' ');
41+
const parts: string[] = authHeader.split(' ');
4642
if (parts.length !== 2 || parts[0] !== 'Bearer') {
4743
return res.status(401).json({
4844
error: 'invalid_token',
4945
error_description: 'Authorization header must be in format: Bearer [token]',
5046
});
5147
}
5248

53-
const token = parts[1];
49+
const token: string = parts[1];
5450

55-
const issuerBase = provider?.baseUrl;
51+
const issuerBase: string | undefined = provider?.baseUrl;
5652

57-
const TOKEN_VALIDATION_CONFIG = {
53+
const TOKEN_VALIDATION_CONFIG: {
54+
jwksUri: string;
55+
options: {
56+
audience?: string;
57+
clockTolerance: number;
58+
issuer: string;
59+
};
60+
} = {
5861
jwksUri: `${issuerBase}/oauth2/jwks`,
5962
options: {
60-
issuer: `${issuerBase}/oauth2/token`,
6163
audience: provider?.clientId,
6264
clockTolerance: 60,
65+
issuer: `${issuerBase}/oauth2/token`,
6366
},
6467
};
6568

@@ -68,7 +71,6 @@ export default function protectedRoute(provider?: McpAuthProvider) {
6871
next();
6972
return undefined;
7073
} catch (error: any) {
71-
console.error('Token validation failed:', error.message);
7274
return res.status(401).json({
7375
error: 'invalid_token',
7476
error_description: error.message || 'Invalid or expired token',

packages/mcp-node/src/constants/authorization-server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,4 @@ export const AUTHORIZATION_SERVER_METADATA_URL: string = '/.well-known/oauth-aut
2626
* The well-known path for OAuth 2.0 Resource Server Metadata.
2727
* @see https://datatracker.ietf.org/doc/html/rfc9728#name-obtaining-protected-resourc
2828
*/
29-
export const PROTECTED_RESOURCE_URL: string = '/.well-known/protected-resource';
29+
export const PROTECTED_RESOURCE_URL: string = '/.well-known/oauth-protected-resource';

packages/mcp-node/src/utils/generate-authorization-server-metadata.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export default function generateAuthorizationServerMetadata(
5757
}
5858

5959
// TODO: Check this further.
60-
metadata.jwks_uri = `${options.baseUrl}/jwks.json`;
60+
metadata.jwks_uri = `${options.baseUrl}/oauth/jwks`;
6161

6262
return metadata;
6363
}

packages/mcp-node/src/utils/validate-access-token.ts

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
*/
1818

1919
import {URL} from 'url';
20-
import {createRemoteJWKSet, jwtVerify, JWTVerifyResult, JWTPayload, JWTVerifyOptions} from 'jose';
20+
import {createRemoteJWKSet, jwtVerify, JWTVerifyResult, JWTPayload, JWTVerifyOptions, ResolvedKey} from 'jose';
2121

2222
export default async function validateAccessToken(
2323
accessToken: string,
@@ -47,13 +47,13 @@ export default async function validateAccessToken(
4747
throw new Error('Audience must be a non-empty string or array of strings in options.');
4848
}
4949

50-
const JWKS = createRemoteJWKSet(jwksUrl);
50+
const JWKS: ReturnType<typeof createRemoteJWKSet> = createRemoteJWKSet(jwksUrl);
5151

5252
try {
53-
const result = await jwtVerify(accessToken, JWKS, {
54-
issuer,
53+
const result: JWTVerifyResult<JWTPayload> & ResolvedKey = await jwtVerify(accessToken, JWKS, {
5554
audience,
5655
clockTolerance,
56+
issuer,
5757
});
5858

5959
const SUPPORTED_SIGNATURE_ALGORITHMS: string[] = ['RS256', 'RS512', 'RS384', 'PS256'];
@@ -70,22 +70,42 @@ export default async function validateAccessToken(
7070
} catch (error: any) {
7171
if (error.code) {
7272
switch (error.code) {
73-
case 'ERR_JOSE_GENERIC':
74-
if (error.message.includes('request failed')) {
75-
throw new Error(`Failed to fetch JWKS from ${jwksUri}: ${error.message}`);
76-
}
77-
break;
78-
case 'ERR_JOSE_NO_KEY_MATCHED':
79-
throw new Error(`No matching key found in JWKS for the token's 'kid' header: ${error.message}`);
80-
case 'ERR_JOSE_JWK_SET_MALFORMED':
81-
throw new Error(`Malformed JWKS found at ${jwksUri}: ${error.message}`);
73+
// JWKS specific issues
74+
case 'ERR_JWKS_TIMEOUT':
75+
throw new Error(`Timeout while fetching JWKS from ${jwksUri}: ${error.message}`);
76+
case 'ERR_JWKS_NO_MATCHING_KEY':
77+
throw new Error(`No matching key found in JWKS at ${jwksUri} for the token's header: ${error.message}`);
78+
case 'ERR_JWKS_INVALID':
79+
throw new Error(`Invalid or malformed JWKS found at ${jwksUri}: ${error.message}`);
80+
81+
// JWS/JWT structural or signature issues
82+
case 'ERR_JWS_INVALID':
83+
throw new Error(`Invalid JWS structure: ${error.message}`);
84+
case 'ERR_JWT_INVALID':
85+
throw new Error(`Invalid JWT structure or payload: ${error.message}`);
86+
case 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED':
87+
throw new Error(`Token signature verification failed: ${error.message}`);
88+
89+
// Common JWT claim validation issues
90+
case 'ERR_JWT_EXPIRED':
91+
throw new Error(`Token has expired: ${error.message}`);
92+
case 'ERR_JWT_CLAIM_VALIDATION_FAILED':
93+
throw new Error(`JWT claim validation failed (${error.claim || 'unknown claim'}): ${error.message}`);
94+
95+
// Other JOSE potential issues
96+
case 'ERR_JOSE_ALG_NOT_ALLOWED':
97+
throw new Error(`Token algorithm is not allowed: ${error.message}`);
98+
case 'ERR_JOSE_NOT_SUPPORTED':
99+
throw new Error(`An unsupported JOSE feature/algorithm was encountered: ${error.message}`);
100+
82101
default:
83-
if (error.code.startsWith('ERR_JWT_')) {
84-
throw new Error(`JWT validation error: ${error.message} (Code: ${error.code})`);
85-
}
102+
throw new Error(`JOSE validation error: ${error.message} (Code: ${error.code})`);
86103
}
87104
}
88105

89-
throw new Error(`An unexpected error occurred during token validation: ${error.message}`);
106+
// Fallback for non-JOSE errors or errors without a code property
107+
throw new Error(
108+
`An unexpected error occurred during token validation: ${error instanceof Error ? error.message : String(error)}`,
109+
);
90110
}
91111
}

0 commit comments

Comments
 (0)