Skip to content

Commit 4f6b5ad

Browse files
pcarletonclaude
andcommitted
feat: add root OAuth discovery fallback with RFC compliance
Implements root OAuth discovery fallback when path-aware discovery fails, restoring compatibility behavior from original code. Adds comprehensive RFC documentation for OAuth 2.0 Authorization Server Metadata (RFC 8414) and OpenID Connect Discovery 1.0 path handling rules. Discovery sequence for issuer with path components: 1. OAuth with path insertion (RFC 8414 Section 3.1) 2. OIDC with path insertion (RFC 8414 Section 5 compatibility) 3. OIDC with path appending (OIDC Discovery 1.0 Section 4.1) 4. OAuth root fallback for compatibility 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 7734124 commit 4f6b5ad

File tree

2 files changed

+77
-4
lines changed

2 files changed

+77
-4
lines changed

src/client/auth.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,52 @@ describe("OAuth Authorization", () => {
774774
expect(calls.length).toBe(2);
775775
});
776776

777+
it("should fall back to root OAuth discovery when path-aware discovery fails", async () => {
778+
// First call (OAuth with path) returns 404
779+
mockFetch.mockResolvedValueOnce({
780+
ok: false,
781+
status: 404,
782+
});
783+
784+
// Second call (OIDC with path insertion) returns 404
785+
mockFetch.mockResolvedValueOnce({
786+
ok: false,
787+
status: 404,
788+
});
789+
790+
// Third call (OIDC with path appending) returns 404
791+
mockFetch.mockResolvedValueOnce({
792+
ok: false,
793+
status: 404,
794+
});
795+
796+
// Fourth call should be OAuth root fallback
797+
mockFetch.mockResolvedValueOnce({
798+
ok: true,
799+
status: 200,
800+
json: async () => validOAuthMetadata,
801+
});
802+
803+
const metadata = await discoverAuthorizationServerMetadata(
804+
"https://mcp.example.com",
805+
"https://auth.example.com/tenant1"
806+
);
807+
808+
expect(metadata).toEqual(validOAuthMetadata);
809+
const calls = mockFetch.mock.calls;
810+
expect(calls.length).toBe(4);
811+
812+
// Should try OAuth with path first
813+
expect(calls[0][0].toString()).toBe("https://auth.example.com/.well-known/oauth-authorization-server/tenant1");
814+
815+
// Should try OIDC discoveries next
816+
expect(calls[1][0].toString()).toBe("https://auth.example.com/.well-known/openid-configuration/tenant1");
817+
expect(calls[2][0].toString()).toBe("https://auth.example.com/tenant1/.well-known/openid-configuration");
818+
819+
// Should finally fall back to OAuth root discovery
820+
expect(calls[3][0].toString()).toBe("https://auth.example.com/.well-known/oauth-authorization-server");
821+
});
822+
777823
it("handles authorization server URL with path in OAuth discovery", async () => {
778824
mockFetch.mockResolvedValueOnce({
779825
ok: true,

src/client/auth.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,9 @@ export async function discoverAuthorizationServerMetadata(
698698
});
699699
}
700700

701+
const url = typeof authorizationServerUrl === 'string' ? new URL(authorizationServerUrl) : authorizationServerUrl;
702+
const hasPath = url.pathname !== '/';
703+
701704
const oauthMetadata = await retrieveOAuthMetadataFromAuthorizationServer(authorizationServerUrl, {
702705
fetchFn,
703706
protocolVersion,
@@ -707,10 +710,26 @@ export async function discoverAuthorizationServerMetadata(
707710
return oauthMetadata;
708711
}
709712

710-
return retrieveOpenIdProviderMetadataFromAuthorizationServer(authorizationServerUrl, {
713+
const oidcMetadata = await retrieveOpenIdProviderMetadataFromAuthorizationServer(authorizationServerUrl, {
711714
fetchFn,
712715
protocolVersion,
713716
});
717+
718+
if (oidcMetadata) {
719+
return oidcMetadata;
720+
}
721+
722+
// If both path-aware discoveries failed and the issuer has a path component,
723+
// try OAuth discovery at the root as a final fallback for compatibility
724+
if (hasPath) {
725+
const rootUrl = new URL(url.origin);
726+
return retrieveOAuthMetadataFromAuthorizationServer(rootUrl, {
727+
fetchFn,
728+
protocolVersion,
729+
});
730+
}
731+
732+
return undefined;
714733
}
715734

716735
/**
@@ -756,8 +775,12 @@ async function retrieveOAuthMetadataFromMcpServer(
756775

757776
/**
758777
* Retrieves RFC 8414 OAuth 2.0 Authorization Server Metadata from the authorization server.
778+
*
779+
* Per RFC 8414 Section 3.1, when the issuer identifier contains path components,
780+
* the well-known URI is constructed by inserting "/.well-known/oauth-authorization-server"
781+
* before the path component.
759782
*
760-
* @param authorizationServerUrl - The authorization server URL
783+
* @param authorizationServerUrl - The authorization server URL (issuer identifier)
761784
* @param options - Configuration options
762785
* @param options.fetchFn - Optional fetch function for making HTTP requests, defaults to global fetch
763786
* @param options.protocolVersion - MCP protocol version to use (required)
@@ -774,7 +797,6 @@ async function retrieveOAuthMetadataFromAuthorizationServer(
774797
}
775798
): Promise<OAuthMetadata | undefined> {
776799
const url = typeof authorizationServerUrl === 'string' ? new URL(authorizationServerUrl) : authorizationServerUrl;
777-
778800
const hasPath = url.pathname !== '/';
779801

780802
const metadataEndpoint = new URL(
@@ -801,8 +823,13 @@ async function retrieveOAuthMetadataFromAuthorizationServer(
801823

802824
/**
803825
* Retrieves OpenID Connect Discovery 1.0 metadata from the authorization server.
826+
*
827+
* Per RFC 8414 Section 5 compatibility notes and OpenID Connect Discovery 1.0 Section 4.1,
828+
* when the issuer identifier contains path components, discovery endpoints are tried in order:
829+
* 1. RFC 8414 style: Insert /.well-known/openid-configuration before the path
830+
* 2. OIDC Discovery 1.0 style: Append /.well-known/openid-configuration after the path
804831
*
805-
* @param authorizationServerUrl - The authorization server URL
832+
* @param authorizationServerUrl - The authorization server URL (issuer identifier)
806833
* @param options - Configuration options
807834
* @param options.fetchFn - Optional fetch function for making HTTP requests, defaults to global fetch
808835
* @param options.protocolVersion - MCP protocol version to use (required)

0 commit comments

Comments
 (0)