Skip to content

Commit 32064a7

Browse files
Move Authorization Server Metadata fetching to oauth.ts (#270797)
* Move Authorization Server Metadata fetching to oauth.ts * Simplified MCP code * Keep all oauth stuff in one place * tests! * Update src/vs/base/test/common/oauth.test.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent fe61d4a commit 32064a7

3 files changed

Lines changed: 562 additions & 80 deletions

File tree

src/vs/base/common/oauth.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1186,3 +1186,117 @@ export async function fetchResourceMetadata(
11861186
throw new AggregateError(errors, 'Failed to fetch resource metadata from all attempted URLs');
11871187
}
11881188
}
1189+
1190+
export interface IFetchAuthorizationServerMetadataOptions {
1191+
/**
1192+
* Headers to include in the requests
1193+
*/
1194+
additionalHeaders?: Record<string, string>;
1195+
/**
1196+
* Optional custom fetch implementation (defaults to global fetch)
1197+
*/
1198+
fetch?: IFetcher;
1199+
}
1200+
1201+
/** Helper to try parsing the response as authorization server metadata */
1202+
async function tryParseAuthServerMetadata(response: CommonResponse): Promise<IAuthorizationServerMetadata | undefined> {
1203+
if (response.status !== 200) {
1204+
return undefined;
1205+
}
1206+
try {
1207+
const body = await response.json();
1208+
if (isAuthorizationServerMetadata(body)) {
1209+
return body;
1210+
}
1211+
} catch {
1212+
// Failed to parse as JSON or not valid metadata
1213+
}
1214+
return undefined;
1215+
}
1216+
1217+
/** Helper to get error text from response */
1218+
async function getErrText(res: CommonResponse): Promise<string> {
1219+
try {
1220+
return await res.text();
1221+
} catch {
1222+
return res.statusText;
1223+
}
1224+
}
1225+
1226+
/**
1227+
* Fetches and validates OAuth 2.0 authorization server metadata from the given authorization server URL.
1228+
*
1229+
* This function tries multiple discovery endpoints in the following order:
1230+
* 1. OAuth 2.0 Authorization Server Metadata with path insertion (RFC 8414)
1231+
* 2. OpenID Connect Discovery with path insertion
1232+
* 3. OpenID Connect Discovery with path addition
1233+
*
1234+
* Path insertion: For issuer URLs with path components (e.g., https://example.com/tenant),
1235+
* the well-known path is inserted after the origin and before the path:
1236+
* https://example.com/.well-known/oauth-authorization-server/tenant
1237+
*
1238+
* Path addition: The well-known path is simply appended to the existing path:
1239+
* https://example.com/tenant/.well-known/openid-configuration
1240+
*
1241+
* @param authorizationServer The authorization server URL (issuer identifier)
1242+
* @param options Configuration options for the fetch operation
1243+
* @returns Promise that resolves to the validated authorization server metadata
1244+
* @throws Error if all discovery attempts fail or the response is invalid
1245+
*
1246+
* @see https://datatracker.ietf.org/doc/html/rfc8414#section-3
1247+
*/
1248+
export async function fetchAuthorizationServerMetadata(
1249+
authorizationServer: string,
1250+
options: IFetchAuthorizationServerMetadataOptions = {}
1251+
): Promise<IAuthorizationServerMetadata> {
1252+
const {
1253+
additionalHeaders = {},
1254+
fetch: fetchImpl = fetch
1255+
} = options;
1256+
1257+
const authorizationServerUrl = new URL(authorizationServer);
1258+
const extraPath = authorizationServerUrl.pathname === '/' ? '' : authorizationServerUrl.pathname;
1259+
1260+
const doFetch = async (url: string): Promise<{ metadata: IAuthorizationServerMetadata | undefined; rawResponse: CommonResponse }> => {
1261+
const rawResponse = await fetchImpl(url, {
1262+
method: 'GET',
1263+
headers: {
1264+
...additionalHeaders,
1265+
'Accept': 'application/json'
1266+
}
1267+
});
1268+
const metadata = await tryParseAuthServerMetadata(rawResponse);
1269+
return { metadata, rawResponse };
1270+
};
1271+
1272+
// For the oauth server metadata discovery path, we _INSERT_
1273+
// the well known path after the origin and before the path.
1274+
// https://datatracker.ietf.org/doc/html/rfc8414#section-3
1275+
const pathToFetch = new URL(AUTH_SERVER_METADATA_DISCOVERY_PATH, authorizationServer).toString() + extraPath;
1276+
let result = await doFetch(pathToFetch);
1277+
if (result.metadata) {
1278+
return result.metadata;
1279+
}
1280+
1281+
// Try fetching the OpenID Connect Discovery with path insertion.
1282+
// For issuer URLs with path components, this inserts the well-known path
1283+
// after the origin and before the path.
1284+
const openidPathInsertionUrl = new URL(OPENID_CONNECT_DISCOVERY_PATH, authorizationServer).toString() + extraPath;
1285+
result = await doFetch(openidPathInsertionUrl);
1286+
if (result.metadata) {
1287+
return result.metadata;
1288+
}
1289+
1290+
// Try fetching the other discovery URL. For the openid metadata discovery
1291+
// path, we _ADD_ the well known path after the existing path.
1292+
// https://datatracker.ietf.org/doc/html/rfc8414#section-3
1293+
const openidPathAdditionUrl = authorizationServer.endsWith('/')
1294+
? authorizationServer + OPENID_CONNECT_DISCOVERY_PATH.substring(1) // Remove leading slash if authServer ends with slash
1295+
: authorizationServer + OPENID_CONNECT_DISCOVERY_PATH;
1296+
result = await doFetch(openidPathAdditionUrl);
1297+
if (result.metadata) {
1298+
return result.metadata;
1299+
}
1300+
1301+
throw new Error(`Failed to fetch authorization server metadata: ${result.rawResponse.status} ${await getErrText(result.rawResponse)}`);
1302+
}

0 commit comments

Comments
 (0)