Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 +1,2 @@
#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!
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#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!
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 @@ -823,7 +823,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