@@ -149,7 +149,7 @@ private Uri ExtractBaseResourceUri(Uri metadataUri)
149149 /// <returns>The resource metadata if the resource matches the server, otherwise throws an exception.</returns>
150150 /// <exception cref="InvalidOperationException">Thrown when the response is not a 401, lacks a WWW-Authenticate header,
151151 /// lacks a resource_metadata parameter, the metadata can't be fetched, or the resource URI doesn't match the server URL.</exception>
152- public async Task < ProtectedResourceMetadata > ExtractProtectedResourceMetadata (
152+ internal async Task < ProtectedResourceMetadata > ExtractProtectedResourceMetadata (
153153 HttpResponseMessage response ,
154154 Uri serverUrl ,
155155 CancellationToken cancellationToken = default )
@@ -169,7 +169,7 @@ public async Task<ProtectedResourceMetadata> ExtractProtectedResourceMetadata(
169169 string ? resourceMetadataUrl = null ;
170170 foreach ( var header in response . Headers . WwwAuthenticate )
171171 {
172- if ( string . Equals ( header . Scheme , "Bearer" , StringComparison . OrdinalIgnoreCase ) &&
172+ if ( string . Equals ( header . Scheme , "Bearer" , StringComparison . OrdinalIgnoreCase ) &&
173173 ! string . IsNullOrEmpty ( header . Parameter ) )
174174 {
175175 resourceMetadataUrl = ParseWwwAuthenticateParameters ( header . Parameter , "resource_metadata" ) ;
@@ -186,17 +186,17 @@ public async Task<ProtectedResourceMetadata> ExtractProtectedResourceMetadata(
186186 }
187187
188188 Uri metadataUri = new ( resourceMetadataUrl ) ;
189-
189+
190190 var metadata = await FetchProtectedResourceMetadataAsync ( metadataUri , cancellationToken ) . ConfigureAwait ( false ) ;
191191 if ( metadata == null )
192192 {
193193 throw new InvalidOperationException ( $ "Failed to fetch resource metadata from { resourceMetadataUrl } ") ;
194194 }
195-
195+
196196 // Extract the base URI from the metadata URL
197197 Uri urlToValidate = ExtractBaseResourceUri ( metadataUri ) ;
198198 _logger . LogDebug ( $ "Validating resource metadata against base URL: { urlToValidate } ") ;
199-
199+
200200 if ( ! VerifyResourceMatch ( metadata , urlToValidate ) )
201201 {
202202 throw new InvalidOperationException (
@@ -245,5 +245,33 @@ public async Task<ProtectedResourceMetadata> ExtractProtectedResourceMetadata(
245245 }
246246
247247 return null ;
248+ } /// <summary>
249+ /// Handles a 401 Unauthorized response and returns all available authorization servers.
250+ /// This is the primary method for OAuth discovery - use this when you want full control
251+ /// over authorization server selection.
252+ /// </summary>
253+ /// <param name="response">The 401 HTTP response.</param>
254+ /// <param name="serverUrl">The server URL that returned the 401.</param>
255+ /// <param name="cancellationToken">A token to cancel the operation.</param>
256+ /// <returns>A list of available authorization server URIs.</returns>
257+ /// <exception cref="ArgumentNullException">Thrown when response is null.</exception>
258+ public async Task < IReadOnlyList < Uri > > GetAvailableAuthorizationServersAsync (
259+ HttpResponseMessage response ,
260+ Uri serverUrl ,
261+ CancellationToken cancellationToken = default )
262+ {
263+ if ( response == null ) throw new ArgumentNullException ( nameof ( response ) ) ;
264+
265+ try
266+ {
267+ // Extract resource metadata behind the scenes
268+ var metadata = await ExtractProtectedResourceMetadata ( response , serverUrl , cancellationToken ) ;
269+ return metadata . AuthorizationServers ?? new List < Uri > ( ) ;
270+ }
271+ catch ( Exception ex )
272+ {
273+ _logger . LogError ( ex , "Failed to get available authorization servers" ) ;
274+ return new List < Uri > ( ) ;
275+ }
248276 }
249277}
0 commit comments