Skip to content

Commit 90f6eca

Browse files
committed
simplify fetching logic
1 parent c8ccd03 commit 90f6eca

File tree

2 files changed

+78
-136
lines changed

2 files changed

+78
-136
lines changed

src/client/auth.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -863,7 +863,7 @@ describe("OAuth Authorization", () => {
863863
).rejects.toThrow("does not support S256 code challenge method required by MCP specification");
864864
});
865865

866-
it("throws on non-404 errors in legacy mode", async () => {
866+
it("throws on non-404 errors", async () => {
867867
mockFetch.mockResolvedValueOnce({
868868
ok: false,
869869
status: 500,

src/client/auth.ts

Lines changed: 77 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -634,7 +634,7 @@ export async function discoverOAuthMetadata(
634634
if (typeof authorizationServerUrl === 'string') {
635635
authorizationServerUrl = new URL(authorizationServerUrl);
636636
}
637-
protocolVersion ??= LATEST_PROTOCOL_VERSION;
637+
protocolVersion ??= LATEST_PROTOCOL_VERSION ;
638638

639639
const response = await discoverMetadataWithFallback(
640640
authorizationServerUrl,
@@ -675,162 +675,104 @@ export async function discoverOAuthMetadata(
675675
* @param options.protocolVersion - MCP protocol version to use, defaults to LATEST_PROTOCOL_VERSION
676676
* @returns Promise resolving to authorization server metadata, or undefined if discovery fails
677677
*/
678-
export async function discoverAuthorizationServerMetadata(
679-
authorizationServerUrl: string | URL,
680-
{
681-
fetchFn = fetch,
682-
protocolVersion = LATEST_PROTOCOL_VERSION,
683-
}: {
684-
fetchFn?: FetchLike;
685-
protocolVersion?: string;
686-
} = {}
687-
): Promise<AuthorizationServerMetadata | undefined> {
678+
/**
679+
* Builds a list of discovery URLs to try for authorization server metadata.
680+
* URLs are returned in priority order:
681+
* 1. OAuth metadata at the given URL
682+
* 2. OAuth metadata at root (if URL has path)
683+
* 3. OIDC metadata endpoints
684+
*/
685+
function buildDiscoveryUrls(authorizationServerUrl: string | URL): { url: URL; type: 'oauth' | 'oidc' }[] {
688686
const url = typeof authorizationServerUrl === 'string' ? new URL(authorizationServerUrl) : authorizationServerUrl;
689687
const hasPath = url.pathname !== '/';
690-
691-
const oauthMetadata = await fetchOAuthMetadata(authorizationServerUrl, {
692-
fetchFn,
693-
protocolVersion,
688+
const urlsToTry: { url: URL; type: 'oauth' | 'oidc' }[] = [];
689+
690+
// 1. OAuth metadata at the given URL
691+
urlsToTry.push({
692+
url: new URL(
693+
buildWellKnownPath('oauth-authorization-server', hasPath ? url.pathname : ''),
694+
url.origin
695+
),
696+
type: 'oauth'
694697
});
695-
696-
if (oauthMetadata) {
697-
return oauthMetadata;
698-
}
699-
698+
699+
// 2. OAuth metadata at root (if URL has path)
700700
if (hasPath) {
701-
const rootUrl = new URL(url.origin);
702-
const rootOauthMetadata = await fetchOAuthMetadata(rootUrl, {
703-
fetchFn,
704-
protocolVersion,
701+
urlsToTry.push({
702+
url: new URL(buildWellKnownPath('oauth-authorization-server'), url.origin),
703+
type: 'oauth'
705704
});
706-
707-
if (rootOauthMetadata) {
708-
return rootOauthMetadata;
709-
}
710-
}
711-
712-
const oidcMetadata = await retrieveOpenIdProviderMetadataFromAuthorizationServer(authorizationServerUrl, {
713-
fetchFn,
714-
protocolVersion,
715-
});
716-
717-
return oidcMetadata;
718-
}
719-
720-
/**
721-
* Retrieves RFC 8414 OAuth 2.0 Authorization Server Metadata from the authorization server.
722-
*
723-
* Per RFC 8414 Section 3.1, when the issuer identifier contains path components,
724-
* the well-known URI is constructed by inserting "/.well-known/oauth-authorization-server"
725-
* before the path component.
726-
*
727-
* @param authorizationServerUrl - The authorization server URL (issuer identifier)
728-
* @param options - Configuration options
729-
* @param options.fetchFn - Optional fetch function for making HTTP requests, defaults to global fetch
730-
* @param options.protocolVersion - MCP protocol version to use (required)
731-
* @returns Promise resolving to OAuth metadata, or undefined if discovery fails
732-
*/
733-
async function fetchOAuthMetadata(
734-
authorizationServerUrl: string | URL,
735-
{
736-
fetchFn = fetch,
737-
protocolVersion,
738-
}: {
739-
fetchFn?: FetchLike;
740-
protocolVersion: string;
741-
}
742-
): Promise<OAuthMetadata | undefined> {
743-
const url = typeof authorizationServerUrl === 'string' ? new URL(authorizationServerUrl) : authorizationServerUrl;
744-
const hasPath = url.pathname !== '/';
745-
746-
const metadataEndpoint = new URL(
747-
buildWellKnownPath('oauth-authorization-server', hasPath ? url.pathname : ''),
748-
url.origin
749-
);
750-
751-
const response = await fetchWithCorsRetry(metadataEndpoint, getProtocolVersionHeader(protocolVersion), fetchFn);
752-
753-
if (!response) {
754-
throw new Error(`CORS error trying to load OAuth metadata from ${metadataEndpoint}`);
755705
}
756-
757-
if (!response.ok) {
758-
if (response.status === 404) {
759-
return undefined;
760-
}
761-
762-
throw new Error(`HTTP ${response.status} trying to load OAuth metadata from ${metadataEndpoint}`);
706+
707+
// 3. OIDC metadata endpoints
708+
if (hasPath) {
709+
// RFC 8414 style: Insert /.well-known/openid-configuration before the path
710+
urlsToTry.push({
711+
url: new URL(buildWellKnownPath('openid-configuration', url.pathname), url.origin),
712+
type: 'oidc'
713+
});
714+
// OIDC Discovery 1.0 style: Append /.well-known/openid-configuration after the path
715+
urlsToTry.push({
716+
url: new URL(buildWellKnownPath('openid-configuration', url.pathname, { prependPathname: true }), url.origin),
717+
type: 'oidc'
718+
});
719+
} else {
720+
urlsToTry.push({
721+
url: new URL(buildWellKnownPath('openid-configuration'), url.origin),
722+
type: 'oidc'
723+
});
763724
}
764-
765-
return OAuthMetadataSchema.parse(await response.json());
725+
726+
return urlsToTry;
766727
}
767728

768-
/**
769-
* Retrieves OpenID Connect Discovery 1.0 metadata from the authorization server.
770-
*
771-
* Per RFC 8414 Section 5 compatibility notes and OpenID Connect Discovery 1.0 Section 4.1,
772-
* when the issuer identifier contains path components, discovery endpoints are tried in order:
773-
* 1. RFC 8414 style: Insert /.well-known/openid-configuration before the path
774-
* 2. OIDC Discovery 1.0 style: Append /.well-known/openid-configuration after the path
775-
*
776-
* @param authorizationServerUrl - The authorization server URL (issuer identifier)
777-
* @param options - Configuration options
778-
* @param options.fetchFn - Optional fetch function for making HTTP requests, defaults to global fetch
779-
* @param options.protocolVersion - MCP protocol version to use (required)
780-
* @returns Promise resolving to OpenID provider metadata, or undefined if discovery fails
781-
*/
782-
async function retrieveOpenIdProviderMetadataFromAuthorizationServer(
729+
export async function discoverAuthorizationServerMetadata(
783730
authorizationServerUrl: string | URL,
784731
{
785732
fetchFn = fetch,
786-
protocolVersion,
733+
protocolVersion = LATEST_PROTOCOL_VERSION,
787734
}: {
788735
fetchFn?: FetchLike;
789-
protocolVersion: string;
790-
}
791-
): Promise<OpenIdProviderDiscoveryMetadata | undefined> {
792-
const url = typeof authorizationServerUrl === 'string' ? new URL(authorizationServerUrl) : authorizationServerUrl;
793-
const hasPath = url.pathname !== '/';
794-
795-
const potentialMetadataEndpoints = hasPath
796-
? [
797-
// https://example.com/.well-known/openid-configuration/tenant1
798-
new URL(buildWellKnownPath('openid-configuration', url.pathname), url.origin),
799-
// https://example.com/tenant1/.well-known/openid-configuration
800-
new URL(buildWellKnownPath('openid-configuration', url.pathname, { prependPathname: true }), `${url.origin}`),
801-
]
802-
: [
803-
// https://example.com/.well-known/openid-configuration
804-
new URL(buildWellKnownPath('openid-configuration'), url.origin),
805-
];
806-
807-
for (const endpoint of potentialMetadataEndpoints) {
808-
const response = await fetchWithCorsRetry(endpoint, getProtocolVersionHeader(protocolVersion), fetchFn);
809-
736+
protocolVersion?: string;
737+
} = {}
738+
): Promise<AuthorizationServerMetadata | undefined> {
739+
const headers = { 'MCP-Protocol-Version': protocolVersion };
740+
741+
// Get the list of URLs to try
742+
const urlsToTry = buildDiscoveryUrls(authorizationServerUrl);
743+
744+
// Try each URL in order
745+
for (const { url: endpointUrl, type } of urlsToTry) {
746+
const response = await fetchWithCorsRetry(endpointUrl, headers, fetchFn);
747+
810748
if (!response) {
811-
throw new Error(`CORS error trying to load OpenID provider metadata from ${endpoint}`);
749+
throw new Error(`CORS error trying to load ${type === 'oauth' ? 'OAuth' : 'OpenID provider'} metadata from ${endpointUrl}`);
812750
}
813-
751+
814752
if (!response.ok) {
815753
if (response.status === 404) {
816-
continue;
754+
continue; // Try next URL
817755
}
818-
819-
throw new Error(`HTTP ${response.status} trying to load OpenID provider metadata from ${endpoint}`);
756+
throw new Error(`HTTP ${response.status} trying to load ${type === 'oauth' ? 'OAuth' : 'OpenID provider'} metadata from ${endpointUrl}`);
820757
}
821-
822-
const metadata = OpenIdProviderDiscoveryMetadataSchema.parse(await response.json());
823-
824-
// MCP spec requires OIDC providers to support S256 PKCE
825-
if (!metadata.code_challenge_methods_supported?.includes('S256')) {
826-
throw new Error(
827-
`Incompatible OIDC provider at ${endpoint}: does not support S256 code challenge method required by MCP specification`
828-
);
758+
759+
// Parse and validate based on type
760+
if (type === 'oauth') {
761+
return OAuthMetadataSchema.parse(await response.json());
762+
} else {
763+
const metadata = OpenIdProviderDiscoveryMetadataSchema.parse(await response.json());
764+
765+
// MCP spec requires OIDC providers to support S256 PKCE
766+
if (!metadata.code_challenge_methods_supported?.includes('S256')) {
767+
throw new Error(
768+
`Incompatible OIDC provider at ${endpointUrl}: does not support S256 code challenge method required by MCP specification`
769+
);
770+
}
771+
772+
return metadata;
829773
}
830-
831-
return metadata;
832774
}
833-
775+
834776
return undefined;
835777
}
836778

0 commit comments

Comments
 (0)