Fix OAuth discovery for servers with non-root resource paths

When the resource URL has a sub-path (e.g. https://mcp.sentry.dev/mcp),
the RFC 8414 path-suffixed well-known URL may not exist. Fall back to
the root well-known URL before giving up.

This matches OpenCode core's behavior and fixes authentication for
servers like Sentry that serve OAuth metadata only at the root path.
This commit is contained in:
sanoyphilippe
2026-03-18 16:45:54 +08:00
parent 23c0ff60f2
commit 0d96e0d3bc
2 changed files with 90 additions and 16 deletions
+27 -16
View File
@@ -36,28 +36,16 @@ async function fetchMetadata(url: string): Promise<{ ok: true; json: Record<stri
return { ok: true, json }
}
async function fetchAuthorizationServerMetadata(issuer: string, resource: string): Promise<OAuthServerMetadata> {
const issuerUrl = parseHttpsUrl(issuer, "Authorization server URL")
const issuerPath = issuerUrl.pathname.replace(/\/+$/, "")
const metadataUrl = new URL(`/.well-known/oauth-authorization-server${issuerPath}`, issuerUrl).toString()
const metadata = await fetchMetadata(metadataUrl)
if (!metadata.ok) {
if (metadata.status === 404) {
throw new Error("OAuth authorization server metadata not found")
}
throw new Error(`OAuth authorization server metadata fetch failed (${metadata.status})`)
}
function parseMetadataFields(json: Record<string, unknown>, resource: string): OAuthServerMetadata {
const authorizationEndpoint = parseHttpsUrl(
readStringField(metadata.json, "authorization_endpoint"),
readStringField(json, "authorization_endpoint"),
"authorization_endpoint"
).toString()
const tokenEndpoint = parseHttpsUrl(
readStringField(metadata.json, "token_endpoint"),
readStringField(json, "token_endpoint"),
"token_endpoint"
).toString()
const registrationEndpointValue = metadata.json.registration_endpoint
const registrationEndpointValue = json.registration_endpoint
const registrationEndpoint =
typeof registrationEndpointValue === "string" && registrationEndpointValue.length > 0
? parseHttpsUrl(registrationEndpointValue, "registration_endpoint").toString()
@@ -71,6 +59,29 @@ async function fetchAuthorizationServerMetadata(issuer: string, resource: string
}
}
async function fetchAuthorizationServerMetadata(issuer: string, resource: string): Promise<OAuthServerMetadata> {
const issuerUrl = parseHttpsUrl(issuer, "Authorization server URL")
const issuerPath = issuerUrl.pathname.replace(/\/+$/, "")
const metadataUrl = new URL(`/.well-known/oauth-authorization-server${issuerPath}`, issuerUrl).toString()
const metadata = await fetchMetadata(metadataUrl)
if (!metadata.ok) {
if (metadata.status === 404 && issuerPath !== "") {
const rootMetadataUrl = new URL("/.well-known/oauth-authorization-server", issuerUrl).toString()
const rootMetadata = await fetchMetadata(rootMetadataUrl)
if (rootMetadata.ok) {
return parseMetadataFields(rootMetadata.json, resource)
}
}
if (metadata.status === 404) {
throw new Error("OAuth authorization server metadata not found")
}
throw new Error(`OAuth authorization server metadata fetch failed (${metadata.status})`)
}
return parseMetadataFields(metadata.json, resource)
}
function parseAuthorizationServers(metadata: Record<string, unknown>): string[] {
const servers = metadata.authorization_servers
if (!Array.isArray(servers)) return []