From 5f177c6fdf3284c14dfebde641c14fd9f8eb6456 Mon Sep 17 00:00:00 2001 From: Bogdan Gavril Date: Tue, 13 Aug 2024 10:58:54 +0300 Subject: [PATCH] Address PR and add UT --- .../IdHelper.cs | 6 +- .../DownstreamApi.cs | 633 +++++++++--------- ...icrosoft.Identity.Web.DownstreamApi.csproj | 3 - .../AppBuilderExtension.cs | 1 + .../MsalAspNetCoreHttpClientFactory.cs | 1 + ...tyWebAppAuthenticationBuilderExtensions.cs | 1 + .../DownstreamApiTests.cs | 40 +- 7 files changed, 362 insertions(+), 323 deletions(-) rename src/{Microsoft.Identity.Web.TokenAcquisition => Microsoft.Identity.Web.Diagnostics}/IdHelper.cs (94%) diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/IdHelper.cs b/src/Microsoft.Identity.Web.Diagnostics/IdHelper.cs similarity index 94% rename from src/Microsoft.Identity.Web.TokenAcquisition/IdHelper.cs rename to src/Microsoft.Identity.Web.Diagnostics/IdHelper.cs index 9275c58c9..dd5591cbc 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/IdHelper.cs +++ b/src/Microsoft.Identity.Web.Diagnostics/IdHelper.cs @@ -6,7 +6,7 @@ using System.Reflection; using System.Text.RegularExpressions; -namespace Microsoft.Identity.Web +namespace Microsoft.Identity.Web.Diagnostics { internal static class IdHelper { @@ -37,13 +37,13 @@ internal static class IdHelper }); public static string GetIdWebVersion() - { + { return s_idWebVersion.Value; } public static string CreateTelemetryInfo() { - return string.Format(CultureInfo.InvariantCulture, IDWebSku + s_idWebVersion.Value); + return string.Format(CultureInfo.InvariantCulture, IDWebSku + GetIdWebVersion()); } } } diff --git a/src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs b/src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs index ffcfe1378..d0815c172 100644 --- a/src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs +++ b/src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs @@ -1,175 +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; - -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 DeserializeOutput(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(); - } - - return await DeserializeOutput(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); - - return await DeserializeOutput(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); - return await DeserializeOutput(response, effectiveOptions).ConfigureAwait(false); - } - +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(); + } + + return await DeserializeOutput(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(); + } + + return await DeserializeOutput(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); + + return await DeserializeOutput(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); + return await DeserializeOutput(response, effectiveOptions).ConfigureAwait(false); + } + #if NET8_0_OR_GREATER /// public async Task CallApiForUserAsync( @@ -263,59 +264,59 @@ public Task CallApiForAppAsync( } #endif - /// - /// Merge the options from configuration and override from caller. - /// - /// Named configuration. - /// Delegate to override the configuration. + /// + /// 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. + 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) - { + 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); } @@ -324,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 { @@ -346,56 +347,56 @@ private DownstreamApiOptions MergeOptions( Encoding.UTF8, "application/json"), }; - } - return httpContent; - } - - internal static async Task DeserializeOutput(HttpResponseMessage response, DownstreamApiOptions effectiveOptions) - where TOutput : class - { + } + return httpContent; + } + + internal static async Task DeserializeOutput(HttpResponseMessage response, DownstreamApiOptions effectiveOptions) + where TOutput : class + { return await DeserializeOutputImpl(response, effectiveOptions, null); } private static async Task DeserializeOutputImpl(HttpResponseMessage response, DownstreamApiOptions effectiveOptions, JsonTypeInfo? outputJsonTypeInfo = null) 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") + { if (outputJsonTypeInfo != null) { return JsonSerializer.Deserialize(stringContent, outputJsonTypeInfo); @@ -405,37 +406,36 @@ private DownstreamApiOptions MergeOptions( return JsonSerializer.Deserialize(stringContent, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); } } - 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; - } - } - + 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(); - AddCallerSDKTelemetry(effectiveOptions); - + 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 @@ -457,8 +457,8 @@ private async Task CallApiInternalAsync( return downstreamApiResult; } - - internal async Task UpdateRequestAsync( + + internal /* internal for test */ async Task UpdateRequestAsync( HttpRequestMessage httpRequestMessage, HttpContent? content, DownstreamApiOptions effectiveOptions, @@ -466,6 +466,8 @@ internal async Task UpdateRequestAsync( ClaimsPrincipal? user, CancellationToken cancellationToken) { + AddCallerSDKTelemetry(effectiveOptions); + if (content != null) { httpRequestMessage.Content = content; @@ -473,32 +475,31 @@ internal async Task UpdateRequestAsync( 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); - } - + } - private static readonly Dictionary s_callerSDKDetails = new() + internal /* for test */ static Dictionary CallerSDKDetails { get; } = new() { { "caller-sdk-id", "1" }, // 1 = Downstream API SDK ID { "caller-sdk-ver", IdHelper.GetIdWebVersion() } @@ -508,15 +509,15 @@ private static void AddCallerSDKTelemetry(DownstreamApiOptions effectiveOptions) { if (effectiveOptions.AcquireTokenOptions.ExtraQueryParameters == null) { - effectiveOptions.AcquireTokenOptions.ExtraQueryParameters = s_callerSDKDetails; + effectiveOptions.AcquireTokenOptions.ExtraQueryParameters = CallerSDKDetails; } else { effectiveOptions.AcquireTokenOptions.ExtraQueryParameters["caller-sdk-id"] = - s_callerSDKDetails["caller-sdk-id"]; + CallerSDKDetails["caller-sdk-id"]; effectiveOptions.AcquireTokenOptions.ExtraQueryParameters["caller-sdk-ver"] = - s_callerSDKDetails["caller-sdk-ver"]; + CallerSDKDetails["caller-sdk-ver"]; } } - } -} + } +} diff --git a/src/Microsoft.Identity.Web.DownstreamApi/Microsoft.Identity.Web.DownstreamApi.csproj b/src/Microsoft.Identity.Web.DownstreamApi/Microsoft.Identity.Web.DownstreamApi.csproj index 86e3ee354..8cf47eeef 100644 --- a/src/Microsoft.Identity.Web.DownstreamApi/Microsoft.Identity.Web.DownstreamApi.csproj +++ b/src/Microsoft.Identity.Web.DownstreamApi/Microsoft.Identity.Web.DownstreamApi.csproj @@ -7,9 +7,6 @@ true README.md - - - True diff --git a/src/Microsoft.Identity.Web.OWIN/AppBuilderExtension.cs b/src/Microsoft.Identity.Web.OWIN/AppBuilderExtension.cs index 55083dbc6..01de5fc9c 100644 --- a/src/Microsoft.Identity.Web.OWIN/AppBuilderExtension.cs +++ b/src/Microsoft.Identity.Web.OWIN/AppBuilderExtension.cs @@ -17,6 +17,7 @@ using Microsoft.Owin.Security.Jwt; using Microsoft.Owin.Security.OAuth; using Microsoft.Owin.Security.OpenIdConnect; +using Microsoft.Identity.Web.Diagnostics; using Owin; namespace Microsoft.Identity.Web diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/MsalAspNetCoreHttpClientFactory.cs b/src/Microsoft.Identity.Web.TokenAcquisition/MsalAspNetCoreHttpClientFactory.cs index 481016003..1537edfb0 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/MsalAspNetCoreHttpClientFactory.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/MsalAspNetCoreHttpClientFactory.cs @@ -3,6 +3,7 @@ using System.Net.Http; using Microsoft.Identity.Client; +using Microsoft.Identity.Web.Diagnostics; namespace Microsoft.Identity.Web { diff --git a/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilderExtensions.cs b/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilderExtensions.cs index 1e731d0c0..3d51521b0 100644 --- a/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilderExtensions.cs +++ b/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilderExtensions.cs @@ -16,6 +16,7 @@ using Microsoft.IdentityModel.Protocols.OpenIdConnect; using System.Linq; using System.Diagnostics.CodeAnalysis; +using Microsoft.Identity.Web.Diagnostics; namespace Microsoft.Identity.Web { diff --git a/tests/Microsoft.Identity.Web.Test/DownstreamWebApiSupport/DownstreamApiTests.cs b/tests/Microsoft.Identity.Web.Test/DownstreamWebApiSupport/DownstreamApiTests.cs index 1dc576884..864642af7 100644 --- a/tests/Microsoft.Identity.Web.Test/DownstreamWebApiSupport/DownstreamApiTests.cs +++ b/tests/Microsoft.Identity.Web.Test/DownstreamWebApiSupport/DownstreamApiTests.cs @@ -49,14 +49,51 @@ public async Task UpdateRequestAsync_WithContent_AddsContentToRequest() // Arrange var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, "https://example.com"); var content = new StringContent("test content"); + var options = new DownstreamApiOptions(); // Act - await _input.UpdateRequestAsync(httpRequestMessage, content, new DownstreamApiOptions(), false, null, CancellationToken.None); + await _input.UpdateRequestAsync(httpRequestMessage, content, options, false, null, CancellationToken.None); // Assert Assert.Equal(content, httpRequestMessage.Content); Assert.Equal("application/json", httpRequestMessage.Headers.Accept.Single().MediaType); Assert.Equal("text/plain", httpRequestMessage.Content?.Headers.ContentType?.MediaType); + Assert.Equal(options.AcquireTokenOptions.ExtraQueryParameters, DownstreamApi.CallerSDKDetails); + } + + + [Fact] + public async Task UpdateRequestAsync_AddsToExtraQP() + { + // Arrange + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, "https://example.com"); + var content = new StringContent("test content"); + var options = new DownstreamApiOptions() { + AcquireTokenOptions = new AcquireTokenOptions() { + ExtraQueryParameters = new Dictionary() + { + { "n1", "v1" }, + { "n2", "v2" }, + { "caller-sdk-id", "bogus" } // value will be overwritten by the SDK + } + } }; + + // Act + await _input.UpdateRequestAsync(httpRequestMessage, content, options, false, null, CancellationToken.None); + + // Assert + Assert.Equal(content, httpRequestMessage.Content); + Assert.Equal("application/json", httpRequestMessage.Headers.Accept.Single().MediaType); + Assert.Equal("text/plain", httpRequestMessage.Content?.Headers.ContentType?.MediaType); + Assert.Equal("v1", options.AcquireTokenOptions.ExtraQueryParameters["n1"]); + Assert.Equal("v2", options.AcquireTokenOptions.ExtraQueryParameters["n2"]); + Assert.Equal( + DownstreamApi.CallerSDKDetails["caller-sdk-id"], + options.AcquireTokenOptions.ExtraQueryParameters["caller-sdk-id"] ); + Assert.Equal( + DownstreamApi.CallerSDKDetails["caller-sdk-ver"], + options.AcquireTokenOptions.ExtraQueryParameters["caller-sdk-ver"]); + } [Theory] @@ -83,6 +120,7 @@ public async Task UpdateRequestAsync_WithScopes_AddsAuthorizationHeaderToRequest Assert.Equal("ey", httpRequestMessage.Headers.Authorization?.Parameter); Assert.Equal("Bearer", httpRequestMessage.Headers.Authorization?.Scheme); Assert.Equal("application/json", httpRequestMessage.Headers.Accept.Single().MediaType); + Assert.Equal(options.AcquireTokenOptions.ExtraQueryParameters, DownstreamApi.CallerSDKDetails); } [Fact]