Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
18 changes: 18 additions & 0 deletions src/server/auth/router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,24 @@ describe('MCP Auth Router', () => {
expect(response.body.scopes_supported).toEqual(['read', 'write']);
expect(response.body.resource_name).toBe('Test API');
});

it('handles path components in issuer URL', async () => {
const app = express();

const options: AuthRouterOptions = {
provider: mockProvider,
issuerUrl: new URL('https://auth.example.com/oauth')
};
app.use(mcpAuthRouter(options));

const response = await supertest(app).get('/.well-known/oauth-authorization-server/oauth');

expect(response.status).toBe(200);
expect(response.body.authorization_endpoint).toBe('https://auth.example.com/oauth/authorize');
expect(response.body.token_endpoint).toBe('https://auth.example.com/oauth/token');
expect(response.body.registration_endpoint).toBe('https://auth.example.com/oauth/register');
expect(response.body.revocation_endpoint).toBe('https://auth.example.com/oauth/revoke');
});
});

describe('Endpoint routing', () => {
Expand Down
29 changes: 19 additions & 10 deletions src/server/auth/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,33 +74,33 @@ export const createOAuthMetadata = (options: {
scopesSupported?: string[];
}): OAuthMetadata => {
const issuer = options.issuerUrl;
const baseUrl = options.baseUrl;
const baseUrl = options.baseUrl || issuer;

checkIssuerUrl(issuer);

const authorization_endpoint = '/authorize';
const token_endpoint = '/token';
const registration_endpoint = options.provider.clientsStore.registerClient ? '/register' : undefined;
const revocation_endpoint = options.provider.revokeToken ? '/revoke' : undefined;
const basePath = baseUrl.pathname.endsWith('/') ? baseUrl.pathname.slice(0, -1) : baseUrl.pathname;

const registration_endpoint = options.provider.clientsStore.registerClient ? new URL(`${basePath}/register`, baseUrl).href : undefined;
const revocation_endpoint = options.provider.revokeToken ? new URL(`${basePath}/revoke`, baseUrl).href : undefined;

const metadata: OAuthMetadata = {
issuer: issuer.href,
service_documentation: options.serviceDocumentationUrl?.href,

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

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

scopes_supported: options.scopesSupported,

revocation_endpoint: revocation_endpoint ? new URL(revocation_endpoint, baseUrl || issuer).href : undefined,
revocation_endpoint,
revocation_endpoint_auth_methods_supported: revocation_endpoint ? ['client_secret_post'] : undefined,

registration_endpoint: registration_endpoint ? new URL(registration_endpoint, baseUrl || issuer).href : undefined
registration_endpoint
};

return metadata;
Expand Down Expand Up @@ -133,6 +133,7 @@ export function mcpAuthRouter(options: AuthRouterOptions): RequestHandler {
router.use(
mcpAuthMetadataRouter({
oauthMetadata,
baseUrl: options.baseUrl,
// Prefer explicit RS; otherwise fall back to AS baseUrl, then to issuer (back-compat)
resourceServerUrl: options.resourceServerUrl ?? options.baseUrl ?? new URL(oauthMetadata.issuer),
serviceDocumentationUrl: options.serviceDocumentationUrl,
Expand Down Expand Up @@ -168,6 +169,13 @@ export type AuthMetadataOptions = {
*/
oauthMetadata: OAuthMetadata;

/**
* The base URL of the authorization server to use for the metadata endpoints.
*
* If not provided, the issuer URL will be used as the base URL.
*/
baseUrl?: URL;

/**
* The url of the MCP server, for use in protected resource metadata
*/
Expand Down Expand Up @@ -209,7 +217,8 @@ export function mcpAuthMetadataRouter(options: AuthMetadataOptions): express.Rou
router.use(`/.well-known/oauth-protected-resource${rsPath === '/' ? '' : rsPath}`, metadataHandler(protectedResourceMetadata));

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

return router;
}
Expand Down
Loading