Skip to content

Commit 117fc07

Browse files
committed
Support issuer URLs with path components
1 parent 2da89db commit 117fc07

File tree

2 files changed

+37
-10
lines changed

2 files changed

+37
-10
lines changed

src/server/auth/router.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,24 @@ describe('MCP Auth Router', () => {
266266
expect(response.body.scopes_supported).toEqual(['read', 'write']);
267267
expect(response.body.resource_name).toBe('Test API');
268268
});
269+
270+
it('handles path components in issuer URL', async () => {
271+
const app = express();
272+
273+
const options: AuthRouterOptions = {
274+
provider: mockProvider,
275+
issuerUrl: new URL('https://auth.example.com/oauth')
276+
};
277+
app.use(mcpAuthRouter(options));
278+
279+
const response = await supertest(app).get('/.well-known/oauth-authorization-server/oauth');
280+
281+
expect(response.status).toBe(200);
282+
expect(response.body.authorization_endpoint).toBe('https://auth.example.com/oauth/authorize');
283+
expect(response.body.token_endpoint).toBe('https://auth.example.com/oauth/token');
284+
expect(response.body.registration_endpoint).toBe('https://auth.example.com/oauth/register');
285+
expect(response.body.revocation_endpoint).toBe('https://auth.example.com/oauth/revoke');
286+
});
269287
});
270288

271289
describe('Endpoint routing', () => {

src/server/auth/router.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,33 +74,33 @@ export const createOAuthMetadata = (options: {
7474
scopesSupported?: string[];
7575
}): OAuthMetadata => {
7676
const issuer = options.issuerUrl;
77-
const baseUrl = options.baseUrl;
77+
const baseUrl = options.baseUrl || issuer;
7878

7979
checkIssuerUrl(issuer);
8080

81-
const authorization_endpoint = '/authorize';
82-
const token_endpoint = '/token';
83-
const registration_endpoint = options.provider.clientsStore.registerClient ? '/register' : undefined;
84-
const revocation_endpoint = options.provider.revokeToken ? '/revoke' : undefined;
81+
const basePath = baseUrl.pathname.endsWith('/') ? baseUrl.pathname.slice(0, -1) : baseUrl.pathname;
82+
83+
const registration_endpoint = options.provider.clientsStore.registerClient ? new URL(`${basePath}/register`, baseUrl).href : undefined;
84+
const revocation_endpoint = options.provider.revokeToken ? new URL(`${basePath}/revoke`, baseUrl).href : undefined;
8585

8686
const metadata: OAuthMetadata = {
8787
issuer: issuer.href,
8888
service_documentation: options.serviceDocumentationUrl?.href,
8989

90-
authorization_endpoint: new URL(authorization_endpoint, baseUrl || issuer).href,
90+
authorization_endpoint: new URL(`${basePath}/authorize`, baseUrl).href,
9191
response_types_supported: ['code'],
9292
code_challenge_methods_supported: ['S256'],
9393

94-
token_endpoint: new URL(token_endpoint, baseUrl || issuer).href,
94+
token_endpoint: new URL(`${basePath}/token`, baseUrl).href,
9595
token_endpoint_auth_methods_supported: ['client_secret_post'],
9696
grant_types_supported: ['authorization_code', 'refresh_token'],
9797

9898
scopes_supported: options.scopesSupported,
9999

100-
revocation_endpoint: revocation_endpoint ? new URL(revocation_endpoint, baseUrl || issuer).href : undefined,
100+
revocation_endpoint,
101101
revocation_endpoint_auth_methods_supported: revocation_endpoint ? ['client_secret_post'] : undefined,
102102

103-
registration_endpoint: registration_endpoint ? new URL(registration_endpoint, baseUrl || issuer).href : undefined
103+
registration_endpoint
104104
};
105105

106106
return metadata;
@@ -133,6 +133,7 @@ export function mcpAuthRouter(options: AuthRouterOptions): RequestHandler {
133133
router.use(
134134
mcpAuthMetadataRouter({
135135
oauthMetadata,
136+
baseUrl: options.baseUrl,
136137
// Prefer explicit RS; otherwise fall back to AS baseUrl, then to issuer (back-compat)
137138
resourceServerUrl: options.resourceServerUrl ?? options.baseUrl ?? new URL(oauthMetadata.issuer),
138139
serviceDocumentationUrl: options.serviceDocumentationUrl,
@@ -168,6 +169,13 @@ export type AuthMetadataOptions = {
168169
*/
169170
oauthMetadata: OAuthMetadata;
170171

172+
/**
173+
* The base URL of the authorization server to use for the metadata endpoints.
174+
*
175+
* If not provided, the issuer URL will be used as the base URL.
176+
*/
177+
baseUrl?: URL;
178+
171179
/**
172180
* The url of the MCP server, for use in protected resource metadata
173181
*/
@@ -209,7 +217,8 @@ export function mcpAuthMetadataRouter(options: AuthMetadataOptions): express.Rou
209217
router.use(`/.well-known/oauth-protected-resource${rsPath === '/' ? '' : rsPath}`, metadataHandler(protectedResourceMetadata));
210218

211219
// Always add this for OAuth Authorization Server metadata per RFC 8414
212-
router.use('/.well-known/oauth-authorization-server', metadataHandler(options.oauthMetadata));
220+
const asPath = new URL(options.baseUrl || options.oauthMetadata.issuer).pathname;
221+
router.use(`/.well-known/oauth-authorization-server${asPath === '/' ? '' : asPath}`, metadataHandler(options.oauthMetadata));
213222

214223
return router;
215224
}

0 commit comments

Comments
 (0)