Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
dd32f62
added initial changes for withattributetoken
4gust Mar 26, 2026
59667b7
added the api in the public api list
4gust Mar 26, 2026
f3967d7
Update src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenOnB…
4gust Mar 26, 2026
bb0dd14
Apply suggestions from code review
4gust Mar 26, 2026
0ed4e23
reduce code duplication, added null safty
4gust Apr 8, 2026
4d646fc
merged with the copilot changes
4gust Apr 8, 2026
a563597
Merge branch 'main' into 4gust/with-attr-token
4gust Apr 8, 2026
169fead
applied copilot suggessions
4gust Apr 8, 2026
4373f36
Merge branch '4gust/with-attr-token' of https://github.com/AzureAD/mi…
4gust Apr 8, 2026
14ae88c
updated the api unshipped doc
4gust Apr 8, 2026
faf858c
moved acquire token to base class
4gust Apr 20, 2026
63fdf9f
resolving comments
4gust Apr 24, 2026
2ebec4f
Update AbstractConfidentialClientAcquireTokenParameterBuilderExtensio…
4gust Apr 24, 2026
3078d92
Restrict WithAttributeTokens to confidential client builders only
Copilot Apr 24, 2026
aa5c089
Merge branch 'main' into 4gust/with-attr-token
4gust Apr 24, 2026
0bbdda7
updated tests and comments on the code.
4gust Apr 24, 2026
df20f25
Merge branch '4gust/with-attr-token' of https://github.com/AzureAD/mi…
4gust Apr 24, 2026
07af457
resolved comments
4gust Apr 27, 2026
1602c6a
updated the withExtraParameter function
4gust Apr 27, 2026
65e6b91
updated the older function for the WithExtraBodyParameters
4gust Apr 27, 2026
513d50d
sorted the token order to maintain the same cache key hit. added tests
4gust Apr 27, 2026
2265042
removed logging check
4gust Apr 28, 2026
9aa3237
resolved comments
4gust Apr 29, 2026
42c89b3
added unit test for cache hit
4gust Apr 29, 2026
6ffc068
Update WithAttributeTokensTests.cs
4gust Apr 29, 2026
8aa44f7
Merge branch 'main' into 4gust/with-attr-token
4gust Apr 29, 2026
9e3c714
Add OBO cache isolation recommendation to WithAttributeTokens XML docs
Copilot Apr 30, 2026
c37263f
Merge branch 'main' into 4gust/with-attr-token
4gust Apr 30, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@ public AcquireTokenForClientParameterBuilder WithAttributes(string attributeJson
{ OAuth2Parameter.Attributes, _ => Task.FromResult(attributeJson) }
};

this.WithExtraBodyParameters(extraBodyParams);
AbstractConfidentialClientAcquireTokenParameterBuilderExtension
.WithExtraBodyParameters<AcquireTokenForClientParameterBuilder>(this, extraBodyParams);

return this;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Client.Core;
using Microsoft.Identity.Client.OAuth2;
Comment thread
4gust marked this conversation as resolved.
Comment thread
4gust marked this conversation as resolved.

namespace Microsoft.Identity.Client.Extensibility
Expand Down Expand Up @@ -42,6 +43,132 @@ public static AbstractAcquireTokenParameterBuilder<T> OnBeforeTokenRequest<T>(
return builder;
}

/// <summary>
/// Sends <paramref name="attributeTokens"/> as the <c>attribute_tokens</c> body parameter
/// and includes them in the cache key. Null/empty/whitespace entries are ignored;
/// a null or empty collection is a no-op.
/// </summary>
/// <typeparam name="T">The concrete confidential client builder type.</typeparam>
/// <param name="builder">The builder to chain options to.</param>
/// <param name="attributeTokens">Attribute tokens to include. Individual tokens must not contain whitespace.</param>
/// <returns>The builder to chain method calls.</returns>
/// <remarks>
/// <para>
/// For <c>AcquireTokenForClient</c> the <c>attribute_tokens</c> set is part of the cache
/// partition key, so different sets are fully isolated.
/// </para>
/// <para>
/// For <c>AcquireTokenOnBehalfOf</c> the partition key is the user assertion hash; the
/// <c>attribute_tokens</c> set is stored as an item-level cache component within that
/// partition. Multiple variants for the same assertion coexist and are disambiguated on
/// read only when the caller supplies the same <c>WithAttributeTokens</c> set.
/// <b>Mixing attributed and non-attributed reads against the same user assertion can
/// return unintended cache entries or fail with <c>multiple_matching_tokens_detected</c>.</b>
/// Be consistent: if you used <c>WithAttributeTokens</c> on a write, use it on every
/// subsequent read for that assertion. Callers that require strict per-set cache isolation
/// across different attribute-token sets should use separate <see cref="IConfidentialClientApplication"/>
/// instances.
/// </para>
/// </remarks>
/// <exception cref="ArgumentException">Thrown when any token contains embedded whitespace.</exception>
Comment thread
4gust marked this conversation as resolved.
/// <exception cref="MsalClientException">
/// Thrown when the application was not configured to allow experimental features
/// (this method transitively calls <see cref="WithExtraBodyParameters{T}"/>, which requires
/// experimental features to be enabled via <c>WithExperimentalFeatures()</c> on the application builder).
/// </exception>
public static T WithAttributeTokens<T>(
this AbstractConfidentialClientAcquireTokenParameterBuilder<T> builder,
IEnumerable<string> attributeTokens)
Comment thread
4gust marked this conversation as resolved.
where T : AbstractConfidentialClientAcquireTokenParameterBuilder<T>
{
ILoggerAdapter logger = builder.ServiceBundle?.ApplicationLogger;

if (attributeTokens is null)
{
logger?.Verbose(() => "[WithAttributeTokens] No attribute tokens passed.");
return (T)builder;
}

var normalizedTokens = new List<string>();
foreach (string token in attributeTokens)
{
if (!string.IsNullOrWhiteSpace(token))
{
string trimmed = token.Trim();
if (trimmed.Any(char.IsWhiteSpace))
{
throw new ArgumentException(
Comment thread
4gust marked this conversation as resolved.
$"Attribute tokens must not contain whitespace. Invalid token: '{trimmed}'",
Comment thread
4gust marked this conversation as resolved.
Comment thread
4gust marked this conversation as resolved.
Comment thread
4gust marked this conversation as resolved.
nameof(attributeTokens));
Comment thread
4gust marked this conversation as resolved.
}

normalizedTokens.Add(trimmed);
}
}

if (normalizedTokens.Count == 0)
{
logger?.Verbose(() => "[WithAttributeTokens] collection contained no usable tokens.");
return (T)builder;
}

// Deduplicate and sort so that callers passing the same logical set of tokens
// (in any order, with possible duplicates) hit the same cache entry and
// produce the same request body.
normalizedTokens = normalizedTokens.Distinct(StringComparer.Ordinal).ToList();
normalizedTokens.Sort(StringComparer.Ordinal);
Comment thread
4gust marked this conversation as resolved.

string joinedTokens = string.Join(" ", normalizedTokens);
Comment thread
4gust marked this conversation as resolved.

int count = normalizedTokens.Count;
logger?.Info(() => $"[WithAttributeTokens] Attaching {count} attribute token(s) " +
"to the request body and including them in the cache key partition.");
Comment thread
4gust marked this conversation as resolved.
Comment thread
4gust marked this conversation as resolved.

var extraBodyParams = new Dictionary<string, Func<CancellationToken, Task<string>>>
{
{ OAuth2Parameter.AttributeTokens, _ => Task.FromResult(joinedTokens) }
Comment thread
4gust marked this conversation as resolved.
};

return builder.WithExtraBodyParameters(extraBodyParams);
}

/// <summary>
/// Add extra body parameters to the token request. These parameters are added to the cache key
/// to associate these parameters with the acquired token. Works for confidential client flows
/// (AcquireTokenForClient, AcquireTokenOnBehalfOf, AcquireTokenByAuthorizationCode).
/// </summary>
/// <typeparam name="T">The concrete confidential client builder type.</typeparam>
/// <param name="builder">The builder to chain options to.</param>
/// <param name="extraBodyParams">List of additional body parameters.</param>
/// <returns>The concrete builder to chain method calls.</returns>
public static T WithExtraBodyParameters<T>(
this AbstractConfidentialClientAcquireTokenParameterBuilder<T> builder,
Dictionary<string, Func<CancellationToken, Task<string>>> extraBodyParams)
where T : AbstractConfidentialClientAcquireTokenParameterBuilder<T>
{
builder.ValidateUseOfExperimentalFeature();
Comment thread
4gust marked this conversation as resolved.

if (extraBodyParams == null || extraBodyParams.Count == 0)
{
return (T)builder;
}

builder.OnBeforeTokenRequest(async data =>
{
foreach (var param in extraBodyParams)
{
if (param.Value != null)
{
data.BodyParameters.Add(param.Key, await param.Value(data.CancellationToken).ConfigureAwait(false));
}
}
});

builder.WithAdditionalCacheKeyComponents(extraBodyParams);

return (T)builder;
}

/// <summary>
/// Binds the token to a key in the cache.No cryptographic operations is performed on the token.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,27 +49,14 @@ public static AcquireTokenForClientParameterBuilder WithProofOfPosessionKeyId(
/// <param name="builder"></param>
/// <param name="extrabodyparams">List of additional body parameters</param>
/// <returns></returns>
[EditorBrowsable(EditorBrowsableState.Never)] // Soft deprecate; prefer the generic overload on AbstractConfidentialClientAcquireTokenParameterBuilderExtension that also supports OBO and AuthorizationCode.
Comment thread
4gust marked this conversation as resolved.
[Obsolete("Use AbstractConfidentialClientAcquireTokenParameterBuilderExtension.WithExtraBodyParameters instead, which also supports OBO and AuthorizationCode flows.", false)]
public static AcquireTokenForClientParameterBuilder WithExtraBodyParameters(
this AcquireTokenForClientParameterBuilder builder,
Dictionary<string, Func<CancellationToken, Task<string>>> extrabodyparams)
Comment thread
4gust marked this conversation as resolved.
{
builder.ValidateUseOfExperimentalFeature();
if (extrabodyparams == null || extrabodyparams.Count == 0)
{
return builder;
}
builder.OnBeforeTokenRequest(async (data) =>
{
foreach (var param in extrabodyparams)
{
if (param.Value != null)
{
data.BodyParameters.Add(param.Key, await param.Value(data.CancellationToken).ConfigureAwait(false));
}
}
});

builder.WithAdditionalCacheKeyComponents(extrabodyparams);
AbstractConfidentialClientAcquireTokenParameterBuilderExtension
.WithExtraBodyParameters<AcquireTokenForClientParameterBuilder>(builder, extrabodyparams);
return builder;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ internal static class OAuth2Parameter
public const string SpaCode = "return_spa_code"; // not a standard OAuth2 param
public const string FmiPath = "fmi_path"; // not a standard OAuth2 param
public const string Attributes = "attributes"; // not a standard OAuth2 param
public const string AttributeTokens = "attribute_tokens"; // not a standard OAuth2 param
public const string UserFederatedIdentityCredential = "user_federated_identity_credential"; // user_fic grant type parameter
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquireTokenParameterBuilderExtension.WithAttributeTokens<T>(this Microsoft.Identity.Client.AbstractConfidentialClientAcquireTokenParameterBuilder<T> builder, System.Collections.Generic.IEnumerable<string> attributeTokens) -> T
static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquireTokenParameterBuilderExtension.WithExtraBodyParameters<T>(this Microsoft.Identity.Client.AbstractConfidentialClientAcquireTokenParameterBuilder<T> builder, System.Collections.Generic.Dictionary<string, System.Func<System.Threading.CancellationToken, System.Threading.Tasks.Task<string>>> extraBodyParams) -> T
Microsoft.Identity.Client.AssertionRequestOptions.CorrelationId.get -> System.Guid
Microsoft.Identity.Client.AssertionRequestOptions.CorrelationId.set -> void
const Microsoft.Identity.Client.MsalError.InvalidCredentialMaterial = "invalid_credential_material" -> string
Microsoft.Identity.Client.AppConfig.CertificateOptions.SendCertificateOverMtls.get -> bool
Microsoft.Identity.Client.AppConfig.CertificateOptions.SendCertificateOverMtls.init -> void
Microsoft.Identity.Client.AssertionRequestOptions.CorrelationId.get -> System.Guid
Microsoft.Identity.Client.AssertionRequestOptions.CorrelationId.set -> void
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquireTokenParameterBuilderExtension.WithAttributeTokens<T>(this Microsoft.Identity.Client.AbstractConfidentialClientAcquireTokenParameterBuilder<T> builder, System.Collections.Generic.IEnumerable<string> attributeTokens) -> T
static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquireTokenParameterBuilderExtension.WithExtraBodyParameters<T>(this Microsoft.Identity.Client.AbstractConfidentialClientAcquireTokenParameterBuilder<T> builder, System.Collections.Generic.Dictionary<string, System.Func<System.Threading.CancellationToken, System.Threading.Tasks.Task<string>>> extraBodyParams) -> T
Microsoft.Identity.Client.AssertionRequestOptions.CorrelationId.get -> System.Guid
Microsoft.Identity.Client.AssertionRequestOptions.CorrelationId.set -> void
const Microsoft.Identity.Client.MsalError.InvalidCredentialMaterial = "invalid_credential_material" -> string
Microsoft.Identity.Client.AppConfig.CertificateOptions.SendCertificateOverMtls.get -> bool
Microsoft.Identity.Client.AppConfig.CertificateOptions.SendCertificateOverMtls.init -> void
Microsoft.Identity.Client.AssertionRequestOptions.CorrelationId.get -> System.Guid
Microsoft.Identity.Client.AssertionRequestOptions.CorrelationId.set -> void
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquireTokenParameterBuilderExtension.WithAttributeTokens<T>(this Microsoft.Identity.Client.AbstractConfidentialClientAcquireTokenParameterBuilder<T> builder, System.Collections.Generic.IEnumerable<string> attributeTokens) -> T
static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquireTokenParameterBuilderExtension.WithExtraBodyParameters<T>(this Microsoft.Identity.Client.AbstractConfidentialClientAcquireTokenParameterBuilder<T> builder, System.Collections.Generic.Dictionary<string, System.Func<System.Threading.CancellationToken, System.Threading.Tasks.Task<string>>> extraBodyParams) -> T
Microsoft.Identity.Client.AssertionRequestOptions.CorrelationId.get -> System.Guid
Microsoft.Identity.Client.AssertionRequestOptions.CorrelationId.set -> void
const Microsoft.Identity.Client.MsalError.InvalidCredentialMaterial = "invalid_credential_material" -> string
Microsoft.Identity.Client.AppConfig.CertificateOptions.SendCertificateOverMtls.get -> bool
Microsoft.Identity.Client.AppConfig.CertificateOptions.SendCertificateOverMtls.init -> void
Microsoft.Identity.Client.AssertionRequestOptions.CorrelationId.get -> System.Guid
Microsoft.Identity.Client.AssertionRequestOptions.CorrelationId.set -> void
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquireTokenParameterBuilderExtension.WithAttributeTokens<T>(this Microsoft.Identity.Client.AbstractConfidentialClientAcquireTokenParameterBuilder<T> builder, System.Collections.Generic.IEnumerable<string> attributeTokens) -> T
static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquireTokenParameterBuilderExtension.WithExtraBodyParameters<T>(this Microsoft.Identity.Client.AbstractConfidentialClientAcquireTokenParameterBuilder<T> builder, System.Collections.Generic.Dictionary<string, System.Func<System.Threading.CancellationToken, System.Threading.Tasks.Task<string>>> extraBodyParams) -> T
Microsoft.Identity.Client.AssertionRequestOptions.CorrelationId.get -> System.Guid
Microsoft.Identity.Client.AssertionRequestOptions.CorrelationId.set -> void
const Microsoft.Identity.Client.MsalError.InvalidCredentialMaterial = "invalid_credential_material" -> string
Microsoft.Identity.Client.AppConfig.CertificateOptions.SendCertificateOverMtls.get -> bool
Microsoft.Identity.Client.AppConfig.CertificateOptions.SendCertificateOverMtls.init -> void
Microsoft.Identity.Client.AssertionRequestOptions.CorrelationId.get -> System.Guid
Microsoft.Identity.Client.AssertionRequestOptions.CorrelationId.set -> void
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquireTokenParameterBuilderExtension.WithAttributeTokens<T>(this Microsoft.Identity.Client.AbstractConfidentialClientAcquireTokenParameterBuilder<T> builder, System.Collections.Generic.IEnumerable<string> attributeTokens) -> T
static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquireTokenParameterBuilderExtension.WithExtraBodyParameters<T>(this Microsoft.Identity.Client.AbstractConfidentialClientAcquireTokenParameterBuilder<T> builder, System.Collections.Generic.Dictionary<string, System.Func<System.Threading.CancellationToken, System.Threading.Tasks.Task<string>>> extraBodyParams) -> T
Microsoft.Identity.Client.AssertionRequestOptions.CorrelationId.get -> System.Guid
Microsoft.Identity.Client.AssertionRequestOptions.CorrelationId.set -> void
const Microsoft.Identity.Client.MsalError.InvalidCredentialMaterial = "invalid_credential_material" -> string
Microsoft.Identity.Client.AppConfig.CertificateOptions.SendCertificateOverMtls.get -> bool
Microsoft.Identity.Client.AppConfig.CertificateOptions.SendCertificateOverMtls.init -> void
Microsoft.Identity.Client.AssertionRequestOptions.CorrelationId.get -> System.Guid
Microsoft.Identity.Client.AssertionRequestOptions.CorrelationId.set -> void
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquireTokenParameterBuilderExtension.WithAttributeTokens<T>(this Microsoft.Identity.Client.AbstractConfidentialClientAcquireTokenParameterBuilder<T> builder, System.Collections.Generic.IEnumerable<string> attributeTokens) -> T
static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquireTokenParameterBuilderExtension.WithExtraBodyParameters<T>(this Microsoft.Identity.Client.AbstractConfidentialClientAcquireTokenParameterBuilder<T> builder, System.Collections.Generic.Dictionary<string, System.Func<System.Threading.CancellationToken, System.Threading.Tasks.Task<string>>> extraBodyParams) -> T
Comment thread
4gust marked this conversation as resolved.
Microsoft.Identity.Client.AssertionRequestOptions.CorrelationId.get -> System.Guid
Microsoft.Identity.Client.AssertionRequestOptions.CorrelationId.set -> void
const Microsoft.Identity.Client.MsalError.InvalidCredentialMaterial = "invalid_credential_material" -> string
Microsoft.Identity.Client.AppConfig.CertificateOptions.SendCertificateOverMtls.get -> bool
Microsoft.Identity.Client.AppConfig.CertificateOptions.SendCertificateOverMtls.init -> void
Microsoft.Identity.Client.AssertionRequestOptions.CorrelationId.get -> System.Guid
Microsoft.Identity.Client.AssertionRequestOptions.CorrelationId.set -> void
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
using Microsoft.Identity.Test.Common.Core.Helpers;
using Microsoft.Identity.Client.Extensibility;

#pragma warning disable CS0618 // Tests intentionally exercise the obsolete AcquireTokenForClientBuilderExtensions.WithExtraBodyParameters overload.

Comment thread
4gust marked this conversation as resolved.
Comment thread
4gust marked this conversation as resolved.
namespace Microsoft.Identity.Test.Unit.PublicApiTests
{
[TestClass]
Expand Down
Loading
Loading