Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Microsoft.Identity.Web.TokenAcquisition/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,8 @@ public static class Constants
/// </summary>
internal const string ClientAssertion = "IDWEB_CLIENT_ASSERTION";

internal const string ExtraBodyParametersKey = "EXTRA_BODY_PARAMETERS";

// Blazor challenge URI
/*
* Used by Microsoft.Identity.Web
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#nullable enable
static Microsoft.Identity.Web.TokenAcquirerExtensions.WithExtraBodyParameters(this Microsoft.Identity.Abstractions.AcquireTokenOptions! options, System.Collections.Generic.IDictionary<string!, string!>! 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.Client.IConfidentialClientApplication!>!
Microsoft.Identity.Web.Extensibility.TokenAcquisitionOptionsExtensions
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#nullable enable
static Microsoft.Identity.Web.TokenAcquirerExtensions.WithExtraBodyParameters(this Microsoft.Identity.Abstractions.AcquireTokenOptions! options, System.Collections.Generic.IDictionary<string!, string!>! 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.Client.IConfidentialClientApplication!>!
Microsoft.Identity.Web.Extensibility.TokenAcquisitionOptionsExtensions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,5 +80,50 @@ public static AcquireTokenOptions WithClientAssertion(this AcquireTokenOptions o

return tokenAcquisitionOptions;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="options">The acquire token options.</param>
/// <param name="extraBodyParameters">The extra body parameters to include in the token request.</param>
/// <returns>The modified options for fluent chaining.</returns>
public static AcquireTokenOptions WithExtraBodyParameters(
this AcquireTokenOptions options,
IDictionary<string, string> extraBodyParameters)
{
if (options is null)
{
throw new System.ArgumentNullException(nameof(options));
}

if (extraBodyParameters is null || extraBodyParameters.Count == 0)
{
return options;
}

options.ExtraParameters ??= new Dictionary<string, object>();

Dictionary<string, System.Func<CancellationToken, Task<string>>> asyncParams;
if (options.ExtraParameters.TryGetValue(Constants.ExtraBodyParametersKey, out var existing) &&
existing is Dictionary<string, System.Func<CancellationToken, Task<string>>> existingDict)
{
asyncParams = existingDict;
}
else
{
asyncParams = new Dictionary<string, System.Func<CancellationToken, Task<string>>>();
options.ExtraParameters[Constants.ExtraBodyParametersKey] = asyncParams;
}

foreach (var kvp in extraBodyParameters)
{
string value = kvp.Value;
Comment thread
4gust marked this conversation as resolved.
asyncParams[kvp.Key] = _ => Task.FromResult(value);
}

return options;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -826,7 +826,7 @@ private async Task<AuthenticationResult> 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<string, Func<CancellationToken, Task<string>>> keyValuePairs)
{
Expand Down
172 changes: 172 additions & 0 deletions tests/Microsoft.Identity.Web.Test/WithExtraBodyParametersTests.cs
Original file line number Diff line number Diff line change
@@ -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<string, string> { { "key", "value" } };

Assert.Throws<ArgumentNullException>(() => 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<string, string>());

Assert.Same(options, result);
}

[Fact]
public async Task WithExtraBodyParameters_AddsParametersToExtraParameters()
{
var options = new AcquireTokenOptions();
var dict = new Dictionary<string, string> { { "key1", "val1" } };

options.WithExtraBodyParameters(dict);

Assert.NotNull(options.ExtraParameters);
Assert.True(options.ExtraParameters.ContainsKey(Constants.ExtraBodyParametersKey));

var asyncParams = options.ExtraParameters[Constants.ExtraBodyParametersKey]
as Dictionary<string, Func<CancellationToken, Task<string>>>;
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<string, string> { { "k", "v" } });

Assert.NotNull(options.ExtraParameters);
}

[Fact]
public async Task WithExtraBodyParameters_MergesWithExistingParameters()
{
var options = new AcquireTokenOptions();

options.WithExtraBodyParameters(new Dictionary<string, string> { { "key1", "val1" } });
options.WithExtraBodyParameters(new Dictionary<string, string> { { "key2", "val2" } });

var asyncParams = options.ExtraParameters![Constants.ExtraBodyParametersKey]
as Dictionary<string, Func<CancellationToken, Task<string>>>;
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<string, string> { { "key1", "original" } });
options.WithExtraBodyParameters(new Dictionary<string, string> { { "key1", "updated" } });

var asyncParams = options.ExtraParameters![Constants.ExtraBodyParametersKey]
as Dictionary<string, Func<CancellationToken, Task<string>>>;
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<string, object>
{
{ "other_key", "other_value" }
}
};

options.WithExtraBodyParameters(new Dictionary<string, string> { { "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<string, string>
{
{ "param1", "value1" },
{ "param2", "value2" },
{ "param3", "value3" }
};

options.WithExtraBodyParameters(dict);

var asyncParams = options.ExtraParameters![Constants.ExtraBodyParametersKey]
as Dictionary<string, Func<CancellationToken, Task<string>>>;
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<string, string> { { "key", "valueA" } });
options2.WithExtraBodyParameters(new Dictionary<string, string> { { "key", "valueB" } });

var asyncParams1 = options1.ExtraParameters![Constants.ExtraBodyParametersKey]
as Dictionary<string, Func<CancellationToken, Task<string>>>;
var asyncParams2 = options2.ExtraParameters![Constants.ExtraBodyParametersKey]
as Dictionary<string, Func<CancellationToken, Task<string>>>;

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);
}
}
}
Loading