diff --git a/src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs b/src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs index c8f80eee8..31c02af2d 100644 --- a/src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs +++ b/src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs @@ -1,176 +1,176 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Runtime.CompilerServices; -using System.Security.Claims; -using System.Text; -using System.Text.Json; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Runtime.CompilerServices; +using System.Security.Claims; +using System.Text; +using System.Text.Json; using System.Text.Json.Serialization.Metadata; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Identity.Abstractions; -using Microsoft.Identity.Client; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Client; using Microsoft.Identity.Web.Diagnostics; - -namespace Microsoft.Identity.Web -{ - /// - internal partial class DownstreamApi : IDownstreamApi - { - private readonly IAuthorizationHeaderProvider _authorizationHeaderProvider; - private readonly IHttpClientFactory _httpClientFactory; - private readonly IOptionsMonitor _namedDownstreamApiOptions; - private const string Authorization = "Authorization"; - protected readonly ILogger _logger; - - /// - /// Constructor. - /// - /// Authorization header provider. - /// Named options provider. - /// HTTP client factory. - /// Logger. - public DownstreamApi( - IAuthorizationHeaderProvider authorizationHeaderProvider, - IOptionsMonitor namedDownstreamApiOptions, - IHttpClientFactory httpClientFactory, - ILogger logger) - { - _authorizationHeaderProvider = authorizationHeaderProvider; - _namedDownstreamApiOptions = namedDownstreamApiOptions; - _httpClientFactory = httpClientFactory; - _logger = logger; - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Task CallApiAsync( - string? serviceName, - Action? downstreamApiOptionsOverride = null, - ClaimsPrincipal? user = null, - HttpContent? content = null, - CancellationToken cancellationToken = default) - { - DownstreamApiOptions effectiveOptions = MergeOptions(serviceName, downstreamApiOptionsOverride); - return CallApiInternalAsync(serviceName, effectiveOptions, effectiveOptions.RequestAppToken, content, - user, cancellationToken); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Task CallApiAsync( - DownstreamApiOptions downstreamApiOptions, - ClaimsPrincipal? user = null, - HttpContent? content = null, - CancellationToken cancellationToken = default) - { - return CallApiInternalAsync(null, downstreamApiOptions, downstreamApiOptions.RequestAppToken, content, - user, cancellationToken); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Task CallApiForUserAsync( - string? serviceName, - Action? downstreamApiOptionsOverride = null, - ClaimsPrincipal? user = null, - HttpContent? content = null, - CancellationToken cancellationToken = default) - { - DownstreamApiOptions effectiveOptions = MergeOptions(serviceName, downstreamApiOptionsOverride); - return CallApiInternalAsync(serviceName, effectiveOptions, false, content, user, cancellationToken); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Task CallApiForAppAsync( - string? serviceName, - Action? downstreamApiOptionsOverride = null, - HttpContent? content = null, - CancellationToken cancellationToken = default) - { - DownstreamApiOptions effectiveOptions = MergeOptions(serviceName, downstreamApiOptionsOverride); - return CallApiInternalAsync(serviceName, effectiveOptions, true, content, null, cancellationToken); - } - - /// - public async Task CallApiForUserAsync( - string? serviceName, - TInput input, - Action? downstreamApiOptionsOverride = null, - ClaimsPrincipal? user = null, - CancellationToken cancellationToken = default) where TOutput : class - { - DownstreamApiOptions effectiveOptions = MergeOptions(serviceName, downstreamApiOptionsOverride); - HttpContent? effectiveInput = SerializeInput(input, effectiveOptions); - - HttpResponseMessage response = await CallApiInternalAsync(serviceName, effectiveOptions, false, - effectiveInput, user, cancellationToken).ConfigureAwait(false); - - // Only dispose the HttpContent if was created here, not provided by the caller. - if (input is not HttpContent) - { - effectiveInput?.Dispose(); - } - + +namespace Microsoft.Identity.Web +{ + /// + internal partial class DownstreamApi : IDownstreamApi + { + private readonly IAuthorizationHeaderProvider _authorizationHeaderProvider; + private readonly IHttpClientFactory _httpClientFactory; + private readonly IOptionsMonitor _namedDownstreamApiOptions; + private const string Authorization = "Authorization"; + protected readonly ILogger _logger; + + /// + /// Constructor. + /// + /// Authorization header provider. + /// Named options provider. + /// HTTP client factory. + /// Logger. + public DownstreamApi( + IAuthorizationHeaderProvider authorizationHeaderProvider, + IOptionsMonitor namedDownstreamApiOptions, + IHttpClientFactory httpClientFactory, + ILogger logger) + { + _authorizationHeaderProvider = authorizationHeaderProvider; + _namedDownstreamApiOptions = namedDownstreamApiOptions; + _httpClientFactory = httpClientFactory; + _logger = logger; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Task CallApiAsync( + string? serviceName, + Action? downstreamApiOptionsOverride = null, + ClaimsPrincipal? user = null, + HttpContent? content = null, + CancellationToken cancellationToken = default) + { + DownstreamApiOptions effectiveOptions = MergeOptions(serviceName, downstreamApiOptionsOverride); + return CallApiInternalAsync(serviceName, effectiveOptions, effectiveOptions.RequestAppToken, content, + user, cancellationToken); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Task CallApiAsync( + DownstreamApiOptions downstreamApiOptions, + ClaimsPrincipal? user = null, + HttpContent? content = null, + CancellationToken cancellationToken = default) + { + return CallApiInternalAsync(null, downstreamApiOptions, downstreamApiOptions.RequestAppToken, content, + user, cancellationToken); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Task CallApiForUserAsync( + string? serviceName, + Action? downstreamApiOptionsOverride = null, + ClaimsPrincipal? user = null, + HttpContent? content = null, + CancellationToken cancellationToken = default) + { + DownstreamApiOptions effectiveOptions = MergeOptions(serviceName, downstreamApiOptionsOverride); + return CallApiInternalAsync(serviceName, effectiveOptions, false, content, user, cancellationToken); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Task CallApiForAppAsync( + string? serviceName, + Action? downstreamApiOptionsOverride = null, + HttpContent? content = null, + CancellationToken cancellationToken = default) + { + DownstreamApiOptions effectiveOptions = MergeOptions(serviceName, downstreamApiOptionsOverride); + return CallApiInternalAsync(serviceName, effectiveOptions, true, content, null, cancellationToken); + } + + /// + public async Task CallApiForUserAsync( + string? serviceName, + TInput input, + Action? downstreamApiOptionsOverride = null, + ClaimsPrincipal? user = null, + CancellationToken cancellationToken = default) where TOutput : class + { + DownstreamApiOptions effectiveOptions = MergeOptions(serviceName, downstreamApiOptionsOverride); + HttpContent? effectiveInput = SerializeInput(input, effectiveOptions); + + HttpResponseMessage response = await CallApiInternalAsync(serviceName, effectiveOptions, false, + effectiveInput, user, cancellationToken).ConfigureAwait(false); + + // Only dispose the HttpContent if was created here, not provided by the caller. + if (input is not HttpContent) + { + effectiveInput?.Dispose(); + } + return await DeserializeOutputAsync(response, effectiveOptions).ConfigureAwait(false); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public async Task CallApiForAppAsync( - string? serviceName, - TInput input, - Action? downstreamApiOptionsOverride = null, - CancellationToken cancellationToken = default) where TOutput : class - { - DownstreamApiOptions effectiveOptions = MergeOptions(serviceName, downstreamApiOptionsOverride); - HttpContent? effectiveInput = SerializeInput(input, effectiveOptions); - HttpResponseMessage response = await CallApiInternalAsync(serviceName, effectiveOptions, true, - effectiveInput, null, cancellationToken).ConfigureAwait(false); - - // Only dispose the HttpContent if was created here, not provided by the caller. - if (input is not HttpContent) - { - effectiveInput?.Dispose(); - } - + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public async Task CallApiForAppAsync( + string? serviceName, + TInput input, + Action? downstreamApiOptionsOverride = null, + CancellationToken cancellationToken = default) where TOutput : class + { + DownstreamApiOptions effectiveOptions = MergeOptions(serviceName, downstreamApiOptionsOverride); + HttpContent? effectiveInput = SerializeInput(input, effectiveOptions); + HttpResponseMessage response = await CallApiInternalAsync(serviceName, effectiveOptions, true, + effectiveInput, null, cancellationToken).ConfigureAwait(false); + + // Only dispose the HttpContent if was created here, not provided by the caller. + if (input is not HttpContent) + { + effectiveInput?.Dispose(); + } + return await DeserializeOutputAsync(response, effectiveOptions).ConfigureAwait(false); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public async Task CallApiForAppAsync(string serviceName, - Action? downstreamApiOptionsOverride = null, - CancellationToken cancellationToken = default) where TOutput : class - { - DownstreamApiOptions effectiveOptions = MergeOptions(serviceName, downstreamApiOptionsOverride); - HttpResponseMessage response = await CallApiInternalAsync(serviceName, effectiveOptions, true, - null, null, cancellationToken).ConfigureAwait(false); - + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public async Task CallApiForAppAsync(string serviceName, + Action? downstreamApiOptionsOverride = null, + CancellationToken cancellationToken = default) where TOutput : class + { + DownstreamApiOptions effectiveOptions = MergeOptions(serviceName, downstreamApiOptionsOverride); + HttpResponseMessage response = await CallApiInternalAsync(serviceName, effectiveOptions, true, + null, null, cancellationToken).ConfigureAwait(false); + return await DeserializeOutputAsync(response, effectiveOptions).ConfigureAwait(false); - } - - /// - public async Task CallApiForUserAsync( - string? serviceName, - Action? downstreamApiOptionsOverride = null, - ClaimsPrincipal? user = null, - CancellationToken cancellationToken = default) where TOutput : class - { - DownstreamApiOptions effectiveOptions = MergeOptions(serviceName, downstreamApiOptionsOverride); - HttpResponseMessage response = await CallApiInternalAsync(serviceName, effectiveOptions, false, - null, user, cancellationToken).ConfigureAwait(false); + } + + /// + public async Task CallApiForUserAsync( + string? serviceName, + Action? downstreamApiOptionsOverride = null, + ClaimsPrincipal? user = null, + CancellationToken cancellationToken = default) where TOutput : class + { + DownstreamApiOptions effectiveOptions = MergeOptions(serviceName, downstreamApiOptionsOverride); + HttpResponseMessage response = await CallApiInternalAsync(serviceName, effectiveOptions, false, + null, user, cancellationToken).ConfigureAwait(false); return await DeserializeOutputAsync(response, effectiveOptions).ConfigureAwait(false); - } - + } + #if NET8_0_OR_GREATER /// public async Task CallApiForUserAsync( @@ -264,59 +264,59 @@ public Task CallApiForAppAsync( } #endif - /// - /// Merge the options from configuration and override from caller. - /// - /// Named configuration. - /// Delegate to override the configuration. - private /* for tests */ DownstreamApiOptions MergeOptions( - string? optionsInstanceName, - Action? calledApiOptionsOverride) - { - // Gets the options from configuration (or default value) - DownstreamApiOptions options; - if (optionsInstanceName != null) - { - options = _namedDownstreamApiOptions.Get(optionsInstanceName); - } - else - { - options = _namedDownstreamApiOptions.CurrentValue; - } - - DownstreamApiOptions clonedOptions = new DownstreamApiOptions(options); - calledApiOptionsOverride?.Invoke(clonedOptions); - return clonedOptions; - } - - /// - /// Merge the options from configuration and override from caller. - /// - /// Named configuration. - /// Delegate to override the configuration. - /// Http method overriding the configuration options. - private DownstreamApiOptions MergeOptions( - string? optionsInstanceName, - Action? calledApiOptionsOverride, HttpMethod httpMethod) - { - // Gets the options from configuration (or default value) - DownstreamApiOptions options; - if (optionsInstanceName != null) - { - options = _namedDownstreamApiOptions.Get(optionsInstanceName); - } - else - { - options = _namedDownstreamApiOptions.CurrentValue; - } - - DownstreamApiOptionsReadOnlyHttpMethod clonedOptions = new DownstreamApiOptionsReadOnlyHttpMethod(options, httpMethod.ToString()); - calledApiOptionsOverride?.Invoke(clonedOptions); - return clonedOptions; - } - - internal static HttpContent? SerializeInput(TInput input, DownstreamApiOptions effectiveOptions) - { + /// + /// Merge the options from configuration and override from caller. + /// + /// Named configuration. + /// Delegate to override the configuration. + internal /* for tests */ DownstreamApiOptions MergeOptions( + string? optionsInstanceName, + Action? calledApiOptionsOverride) + { + // Gets the options from configuration (or default value) + DownstreamApiOptions options; + if (optionsInstanceName != null) + { + options = _namedDownstreamApiOptions.Get(optionsInstanceName); + } + else + { + options = _namedDownstreamApiOptions.CurrentValue; + } + + DownstreamApiOptions clonedOptions = new DownstreamApiOptions(options); + calledApiOptionsOverride?.Invoke(clonedOptions); + return clonedOptions; + } + + /// + /// Merge the options from configuration and override from caller. + /// + /// Named configuration. + /// Delegate to override the configuration. + /// Http method overriding the configuration options. + internal /* for tests */ DownstreamApiOptions MergeOptions( + string? optionsInstanceName, + Action? calledApiOptionsOverride, HttpMethod httpMethod) + { + // Gets the options from configuration (or default value) + DownstreamApiOptions options; + if (optionsInstanceName != null) + { + options = _namedDownstreamApiOptions.Get(optionsInstanceName); + } + else + { + options = _namedDownstreamApiOptions.CurrentValue; + } + + DownstreamApiOptionsReadOnlyHttpMethod clonedOptions = new DownstreamApiOptionsReadOnlyHttpMethod(options, httpMethod.ToString()); + calledApiOptionsOverride?.Invoke(clonedOptions); + return clonedOptions; + } + + internal static HttpContent? SerializeInput(TInput input, DownstreamApiOptions effectiveOptions) + { return SerializeInputImpl(input, effectiveOptions, null); } @@ -325,11 +325,11 @@ private DownstreamApiOptions MergeOptions( HttpContent? httpContent; if (effectiveOptions.Serializer != null) - { + { httpContent = effectiveOptions.Serializer(input); - } - else - { + } + else + { // if the input is already an HttpContent, it's used as is, and should already contain a ContentType. httpContent = input switch { @@ -347,13 +347,13 @@ private DownstreamApiOptions MergeOptions( Encoding.UTF8, "application/json"), }; - } - return httpContent; - } - + } + return httpContent; + } + internal static async Task DeserializeOutputAsync(HttpResponseMessage response, DownstreamApiOptions effectiveOptions) - where TOutput : class - { + where TOutput : class + { try { response.EnsureSuccessStatusCode(); @@ -405,75 +405,75 @@ private DownstreamApiOptions MergeOptions( private static async Task DeserializeOutputImplAsync(HttpResponseMessage response, DownstreamApiOptions effectiveOptions, JsonTypeInfo outputJsonTypeInfo) where TOutput : class { - try - { - response.EnsureSuccessStatusCode(); - } - catch - { - string error = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - -#if NET5_0_OR_GREATER - throw new HttpRequestException($"{(int)response.StatusCode} {response.StatusCode} {error}", null, response.StatusCode); -#else - throw new HttpRequestException($"{(int)response.StatusCode} {response.StatusCode} {error}"); -#endif - } - - HttpContent content = response.Content; - - if (content == null) - { - return default; - } - - string? mediaType = content.Headers.ContentType?.MediaType; - - if (effectiveOptions.Deserializer != null) - { - return effectiveOptions.Deserializer(content) as TOutput; - } + try + { + response.EnsureSuccessStatusCode(); + } + catch + { + string error = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + +#if NET5_0_OR_GREATER + throw new HttpRequestException($"{(int)response.StatusCode} {response.StatusCode} {error}", null, response.StatusCode); +#else + throw new HttpRequestException($"{(int)response.StatusCode} {response.StatusCode} {error}"); +#endif + } + + HttpContent content = response.Content; + + if (content == null) + { + return default; + } + + string? mediaType = content.Headers.ContentType?.MediaType; + + if (effectiveOptions.Deserializer != null) + { + return effectiveOptions.Deserializer(content) as TOutput; + } else if (typeof(TOutput).IsAssignableFrom(typeof(HttpContent))) { return content as TOutput; - } - else - { - string stringContent = await content.ReadAsStringAsync(); - if (mediaType == "application/json") - { + } + else + { + string stringContent = await content.ReadAsStringAsync(); + if (mediaType == "application/json") + { return JsonSerializer.Deserialize(stringContent, outputJsonTypeInfo); } - if (mediaType != null && !mediaType.StartsWith("text/", StringComparison.OrdinalIgnoreCase)) - { - // Handle other content types here - throw new NotSupportedException("Content type not supported. Provide your own deserializer. "); - } - return stringContent as TOutput; - } - } - - private async Task CallApiInternalAsync( - string? serviceName, - DownstreamApiOptions effectiveOptions, - bool appToken, - HttpContent? content = null, - ClaimsPrincipal? user = null, - CancellationToken cancellationToken = default) - { - // Downstream API URI - string apiUrl = effectiveOptions.GetApiUrl(); - + if (mediaType != null && !mediaType.StartsWith("text/", StringComparison.OrdinalIgnoreCase)) + { + // Handle other content types here + throw new NotSupportedException("Content type not supported. Provide your own deserializer. "); + } + return stringContent as TOutput; + } + } + + internal /* for tests */ async Task CallApiInternalAsync( + string? serviceName, + DownstreamApiOptions effectiveOptions, + bool appToken, + HttpContent? content = null, + ClaimsPrincipal? user = null, + CancellationToken cancellationToken = default) + { + // Downstream API URI + string apiUrl = effectiveOptions.GetApiUrl(); + // Create an HTTP request message using HttpRequestMessage httpRequestMessage = new( - new HttpMethod(effectiveOptions.HttpMethod), - apiUrl); - - await UpdateRequestAsync(httpRequestMessage, content, effectiveOptions, appToken, user, cancellationToken); - + new HttpMethod(effectiveOptions.HttpMethod), + apiUrl); + + await UpdateRequestAsync(httpRequestMessage, content, effectiveOptions, appToken, user, cancellationToken); + using HttpClient client = string.IsNullOrEmpty(serviceName) ? _httpClientFactory.CreateClient() : _httpClientFactory.CreateClient(serviceName); - - // Send the HTTP message + + // Send the HTTP message var downstreamApiResult = await client.SendAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false); // Retry only if the resource sent 401 Unauthorized with WWW-Authenticate header and claims @@ -495,7 +495,7 @@ private async Task CallApiInternalAsync( return downstreamApiResult; } - + internal /* internal for test */ async Task UpdateRequestAsync( HttpRequestMessage httpRequestMessage, HttpContent? content, @@ -513,29 +513,29 @@ private async Task CallApiInternalAsync( effectiveOptions.RequestAppToken = appToken; - // Obtention of the authorization header (except when calling an anonymous endpoint - // which is done by not specifying any scopes - if (effectiveOptions.Scopes != null && effectiveOptions.Scopes.Any()) - { + // Obtention of the authorization header (except when calling an anonymous endpoint + // which is done by not specifying any scopes + if (effectiveOptions.Scopes != null && effectiveOptions.Scopes.Any()) + { string authorizationHeader = await _authorizationHeaderProvider.CreateAuthorizationHeaderAsync( effectiveOptions.Scopes, effectiveOptions, user, - cancellationToken).ConfigureAwait(false); + cancellationToken).ConfigureAwait(false); httpRequestMessage.Headers.Add(Authorization, authorizationHeader); - } - else - { - Logger.UnauthenticatedApiCall(_logger, null); - } - if (!string.IsNullOrEmpty(effectiveOptions.AcceptHeader)) + } + else + { + Logger.UnauthenticatedApiCall(_logger, null); + } + if (!string.IsNullOrEmpty(effectiveOptions.AcceptHeader)) { httpRequestMessage.Headers.Accept.ParseAdd(effectiveOptions.AcceptHeader); - } - // Opportunity to change the request message + } + // Opportunity to change the request message effectiveOptions.CustomizeHttpRequestMessage?.Invoke(httpRequestMessage); - } + } internal /* for test */ static Dictionary CallerSDKDetails { get; } = new() { @@ -557,5 +557,5 @@ private static void AddCallerSDKTelemetry(DownstreamApiOptions effectiveOptions) CallerSDKDetails["caller-sdk-ver"]; } } - } -} + } +}