@@ -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