diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/Constants.cs b/src/Microsoft.Identity.Web.TokenAcquisition/Constants.cs index 0268e137a..b7c9f5fcb 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/Constants.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/Constants.cs @@ -264,6 +264,8 @@ public static class Constants /// internal const string ClientAssertion = "IDWEB_CLIENT_ASSERTION"; + internal const string ExtraBodyParametersKey = "EXTRA_BODY_PARAMETERS"; + // Blazor challenge URI /* * Used by Microsoft.Identity.Web diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/NetCore/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/NetCore/PublicAPI.Unshipped.txt index 5824eb06f..a205d4fe6 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/NetCore/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/NetCore/PublicAPI.Unshipped.txt @@ -1,4 +1,5 @@ #nullable enable +static Microsoft.Identity.Web.TokenAcquirerExtensions.WithExtraBodyParameters(this Microsoft.Identity.Abstractions.AcquireTokenOptions! options, System.Collections.Generic.IDictionary! extraBodyParameters) -> Microsoft.Identity.Abstractions.AcquireTokenOptions! Microsoft.Identity.Web.Extensibility.IConfidentialClientApplicationProvider Microsoft.Identity.Web.Extensibility.IConfidentialClientApplicationProvider.GetConfidentialClientApplicationAsync(string? authenticationScheme = null) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.Extensibility.TokenAcquisitionOptionsExtensions diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/NetFramework/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/NetFramework/PublicAPI.Unshipped.txt index 5824eb06f..a205d4fe6 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/NetFramework/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/NetFramework/PublicAPI.Unshipped.txt @@ -1,4 +1,5 @@ #nullable enable +static Microsoft.Identity.Web.TokenAcquirerExtensions.WithExtraBodyParameters(this Microsoft.Identity.Abstractions.AcquireTokenOptions! options, System.Collections.Generic.IDictionary! extraBodyParameters) -> Microsoft.Identity.Abstractions.AcquireTokenOptions! Microsoft.Identity.Web.Extensibility.IConfidentialClientApplicationProvider Microsoft.Identity.Web.Extensibility.IConfidentialClientApplicationProvider.GetConfidentialClientApplicationAsync(string? authenticationScheme = null) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.Extensibility.TokenAcquisitionOptionsExtensions diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquirerExtensions.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquirerExtensions.cs index 52edcfdc1..458184f57 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquirerExtensions.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquirerExtensions.cs @@ -80,5 +80,50 @@ public static AcquireTokenOptions WithClientAssertion(this AcquireTokenOptions o return tokenAcquisitionOptions; } + + /// + /// Adds extra body parameters to the token acquisition request. + /// Parameters are merged into any existing extra body parameters dictionary, + /// so this can be composed with other extensions that also add body parameters. + /// + /// The acquire token options. + /// The extra body parameters to include in the token request. + /// The modified options for fluent chaining. + public static AcquireTokenOptions WithExtraBodyParameters( + this AcquireTokenOptions options, + IDictionary extraBodyParameters) + { + if (options is null) + { + throw new System.ArgumentNullException(nameof(options)); + } + + if (extraBodyParameters is null || extraBodyParameters.Count == 0) + { + return options; + } + + options.ExtraParameters ??= new Dictionary(); + + Dictionary>> asyncParams; + if (options.ExtraParameters.TryGetValue(Constants.ExtraBodyParametersKey, out var existing) && + existing is Dictionary>> existingDict) + { + asyncParams = existingDict; + } + else + { + asyncParams = new Dictionary>>(); + options.ExtraParameters[Constants.ExtraBodyParametersKey] = asyncParams; + } + + foreach (var kvp in extraBodyParameters) + { + string value = kvp.Value; + asyncParams[kvp.Key] = _ => Task.FromResult(value); + } + + return options; + } } } diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs index eff4fb715..ddc96fd65 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs @@ -826,7 +826,7 @@ private async Task GetAuthenticationResultForAppInternalAs private void AddExtraBodyParametersIfNeeded(TokenAcquisitionOptions tokenAcquisitionOptions, AcquireTokenForClientParameterBuilder builder) { if (tokenAcquisitionOptions.ExtraParameters != null - && tokenAcquisitionOptions.ExtraParameters.TryGetValue("EXTRA_BODY_PARAMETERS", out object? parameters)) + && tokenAcquisitionOptions.ExtraParameters.TryGetValue(Constants.ExtraBodyParametersKey, out object? parameters)) { if (parameters is Dictionary>> keyValuePairs) { diff --git a/tests/Microsoft.Identity.Web.Test/WithExtraBodyParametersTests.cs b/tests/Microsoft.Identity.Web.Test/WithExtraBodyParametersTests.cs new file mode 100644 index 000000000..3011f4710 --- /dev/null +++ b/tests/Microsoft.Identity.Web.Test/WithExtraBodyParametersTests.cs @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Identity.Abstractions; +using Xunit; + +namespace Microsoft.Identity.Web.Test +{ + public class WithExtraBodyParametersTests + { + [Fact] + public void WithExtraBodyParameters_NullOptions_ThrowsArgumentNullException() + { + AcquireTokenOptions options = null!; + var dict = new Dictionary { { "key", "value" } }; + + Assert.Throws(() => options.WithExtraBodyParameters(dict)); + } + + [Fact] + public void WithExtraBodyParameters_NullDictionary_ReturnsSameOptions() + { + var options = new AcquireTokenOptions(); + + var result = options.WithExtraBodyParameters(null!); + + Assert.Same(options, result); + } + + [Fact] + public void WithExtraBodyParameters_EmptyDictionary_ReturnsSameOptions() + { + var options = new AcquireTokenOptions(); + + var result = options.WithExtraBodyParameters(new Dictionary()); + + Assert.Same(options, result); + } + + [Fact] + public async Task WithExtraBodyParameters_AddsParametersToExtraParameters() + { + var options = new AcquireTokenOptions(); + var dict = new Dictionary { { "key1", "val1" } }; + + options.WithExtraBodyParameters(dict); + + Assert.NotNull(options.ExtraParameters); + Assert.True(options.ExtraParameters.ContainsKey(Constants.ExtraBodyParametersKey)); + + var asyncParams = options.ExtraParameters[Constants.ExtraBodyParametersKey] + as Dictionary>>; + Assert.NotNull(asyncParams); + Assert.True(asyncParams.ContainsKey("key1")); + Assert.Equal("val1", await asyncParams["key1"](CancellationToken.None)); + } + + [Fact] + public void WithExtraBodyParameters_InitializesExtraParametersIfNull() + { + var options = new AcquireTokenOptions(); + Assert.Null(options.ExtraParameters); + + options.WithExtraBodyParameters(new Dictionary { { "k", "v" } }); + + Assert.NotNull(options.ExtraParameters); + } + + [Fact] + public async Task WithExtraBodyParameters_MergesWithExistingParameters() + { + var options = new AcquireTokenOptions(); + + options.WithExtraBodyParameters(new Dictionary { { "key1", "val1" } }); + options.WithExtraBodyParameters(new Dictionary { { "key2", "val2" } }); + + var asyncParams = options.ExtraParameters![Constants.ExtraBodyParametersKey] + as Dictionary>>; + Assert.NotNull(asyncParams); + Assert.Equal(2, asyncParams.Count); + Assert.Equal("val1", await asyncParams["key1"](CancellationToken.None)); + Assert.Equal("val2", await asyncParams["key2"](CancellationToken.None)); + } + + [Fact] + public async Task WithExtraBodyParameters_OverwritesExistingKey() + { + var options = new AcquireTokenOptions(); + + options.WithExtraBodyParameters(new Dictionary { { "key1", "original" } }); + options.WithExtraBodyParameters(new Dictionary { { "key1", "updated" } }); + + var asyncParams = options.ExtraParameters![Constants.ExtraBodyParametersKey] + as Dictionary>>; + Assert.NotNull(asyncParams); + Assert.Single(asyncParams); + Assert.Equal("updated", await asyncParams["key1"](CancellationToken.None)); + } + + [Fact] + public void WithExtraBodyParameters_PreservesOtherExtraParameters() + { + var options = new AcquireTokenOptions + { + ExtraParameters = new Dictionary + { + { "other_key", "other_value" } + } + }; + + options.WithExtraBodyParameters(new Dictionary { { "key1", "val1" } }); + + Assert.True(options.ExtraParameters.ContainsKey("other_key")); + Assert.Equal("other_value", options.ExtraParameters["other_key"]); + Assert.True(options.ExtraParameters.ContainsKey(Constants.ExtraBodyParametersKey)); + } + + [Fact] + public async Task WithExtraBodyParameters_AsyncFuncsReturnCorrectValues() + { + var options = new AcquireTokenOptions(); + var dict = new Dictionary + { + { "param1", "value1" }, + { "param2", "value2" }, + { "param3", "value3" } + }; + + options.WithExtraBodyParameters(dict); + + var asyncParams = options.ExtraParameters![Constants.ExtraBodyParametersKey] + as Dictionary>>; + Assert.NotNull(asyncParams); + Assert.Equal(3, asyncParams.Count); + + foreach (var kvp in dict) + { + string result = await asyncParams[kvp.Key](CancellationToken.None); + Assert.Equal(kvp.Value, result); + } + } + + [Fact] + public async Task WithExtraBodyParameters_DifferentParamsProduceDifferentCacheEntries() + { + var options1 = new AcquireTokenOptions(); + var options2 = new AcquireTokenOptions(); + + options1.WithExtraBodyParameters(new Dictionary { { "key", "valueA" } }); + options2.WithExtraBodyParameters(new Dictionary { { "key", "valueB" } }); + + var asyncParams1 = options1.ExtraParameters![Constants.ExtraBodyParametersKey] + as Dictionary>>; + var asyncParams2 = options2.ExtraParameters![Constants.ExtraBodyParametersKey] + as Dictionary>>; + + Assert.NotNull(asyncParams1); + Assert.NotNull(asyncParams2); + Assert.NotSame(asyncParams1, asyncParams2); + + string val1 = await asyncParams1["key"](CancellationToken.None); + string val2 = await asyncParams2["key"](CancellationToken.None); + Assert.NotEqual(val1, val2); + Assert.Equal("valueA", val1); + Assert.Equal("valueB", val2); + } + } +}