Skip to content

Added support for WithArributeTokens #5888

Open
4gust wants to merge 10 commits intomainfrom
4gust/with-attr-token
Open

Added support for WithArributeTokens #5888
4gust wants to merge 10 commits intomainfrom
4gust/with-attr-token

Conversation

@4gust
Copy link
Copy Markdown
Contributor

@4gust 4gust commented Mar 26, 2026

Added support for WithAttributeTokens for CCA acquisition methods

This pull request adds support for specifying attribute tokens in token acquisition requests across multiple authentication flows in the Microsoft Identity Client library. It introduces new WithAttributeTokens methods to the parameter builders for authorization code, client credentials, and on-behalf-of flows, allowing developers to include custom attribute tokens in requests via the attribute_tokens parameter.

The most important changes include:

New API Surface:

  • Added WithAttributeTokens(IEnumerable<string> attributeTokens) methods to AcquireTokenByAuthorizationCodeParameterBuilder, AcquireTokenForClientParameterBuilder, and AcquireTokenOnBehalfOfParameterBuilder, enabling the inclusion of attribute tokens in token requests. These methods validate input, join the tokens, and add them to the request body. [1] [2] [3]

  • Updated the public API files for all supported frameworks to register the new WithAttributeTokens methods, ensuring they are part of the public API surface.

OAuth2 Parameter Support:

  • Added a new constant AttributeTokens to the OAuth2Parameter class for use as the key in the request body parameter.

Fixes #

Testing
Unit test added.

@4gust 4gust requested a review from a team as a code owner March 26, 2026 15:18
Copilot AI review requested due to automatic review settings March 26, 2026 15:18
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds support for sending attribute_tokens on confidential-client token requests (auth code, client credentials, and OBO) via new WithAttributeTokens(...) builder APIs, updates the public API baselines, and introduces unit tests validating request-body behavior and (for client credentials) cache partitioning.

Changes:

  • Added WithAttributeTokens(IEnumerable<string>) to auth code, client credentials, and OBO parameter builders.
  • Introduced OAuth2Parameter.AttributeTokens constant ("attribute_tokens").
  • Updated PublicAPI Unshipped baselines across TFMs and added new unit tests.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
tests/Microsoft.Identity.Test.Unit/PublicApiTests/WithAttributeTokensTests.cs Adds unit coverage for WithAttributeTokens formatting, null validation, and client-credentials caching behavior.
src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenByAuthorizationCodeParameterBuilder.cs Adds auth-code builder API to attach attribute_tokens to the token request body.
src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForClientParameterBuilder.cs Adds client-credentials builder API to attach attribute_tokens (via extra body params).
src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenOnBehalfOfParameterBuilder.cs Adds OBO builder API to attach attribute_tokens to the token request body.
src/client/Microsoft.Identity.Client/OAuth2/OAuthConstants.cs Adds OAuth2Parameter.AttributeTokens constant.
src/client/Microsoft.Identity.Client/PublicApi/*/PublicAPI.Unshipped.txt Registers new public API methods for all supported TFMs.

Comment on lines +214 to +218
[TestMethod]
public async Task WithAttributeTokens_OnBehalfOf_AddsSpaceSeparatedTokensToBody_Async()
{
using (var httpManager = new MockHttpManager())
{
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are tests verifying request-body formatting and cache-key partitioning for client-credentials, but nothing validating cache behavior for OBO (which does read from cache by default). Add a unit test that makes two OBO requests with the same scopes/assertion but different attribute tokens and asserts the second does not return a cache hit (and that two distinct AT entries are stored).

Copilot uses AI. Check for mistakes.
Comment on lines +180 to +184
string joinedTokens = string.Join(" ", attributeTokens);

this.OnBeforeTokenRequest(async (data) =>
{
data.BodyParameters.Add(OAuth2Parameter.AttributeTokens, joinedTokens);
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AcquireTokenOnBehalfOf looks up access tokens in the cache before sending the token request, but WithAttributeTokens only adds attribute_tokens to the request body. This means requests with different attribute tokens can incorrectly hit the same cached access token and return a token minted for a different set of attribute tokens. Add the joined attribute tokens to the request cache key components as well (e.g., via WithAdditionalCacheKeyComponents).

Copilot uses AI. Check for mistakes.
Comment on lines +157 to +172
/// <summary>
/// Specifies attribute tokens to include in the token request.
/// The tokens are joined with spaces and sent as the <c>attribute_tokens</c> body parameter.
/// </summary>
/// <param name="attributeTokens">A list of attribute token strings to include in the request.</param>
/// <returns>The builder to chain method calls.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="attributeTokens"/> is null or contains no elements.</exception>
public AcquireTokenByAuthorizationCodeParameterBuilder WithAttributeTokens(IEnumerable<string> attributeTokens)
{
if (attributeTokens == null)
{
throw new ArgumentNullException(nameof(attributeTokens));
}

string joinedTokens = string.Join(" ", attributeTokens);

Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The XML docs say WithAttributeTokens throws when attributeTokens is null or contains no elements, but the implementation only checks for null. Passing an empty enumerable results in attribute_tokens being set to an empty string. Either validate non-empty input (and ideally non-null/non-whitespace tokens) or adjust the documentation/behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +164 to +178
/// <summary>
/// Specifies attribute tokens to include in the token request.
/// The tokens are joined with spaces and sent as the <c>attribute_tokens</c> body parameter.
/// </summary>
/// <param name="attributeTokens">A list of attribute token strings to include in the request.</param>
/// <returns>The builder to chain method calls.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="attributeTokens"/> is null or contains no elements.</exception>
public AcquireTokenForClientParameterBuilder WithAttributeTokens(IEnumerable<string> attributeTokens)
{
if (attributeTokens == null)
{
throw new ArgumentNullException(nameof(attributeTokens));
}

string joinedTokens = string.Join(" ", attributeTokens);
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The XML docs say WithAttributeTokens throws when attributeTokens is null or contains no elements, but the implementation only checks for null. An empty enumerable will result in attribute_tokens being sent as an empty string. Either validate non-empty input (and ideally non-null/non-whitespace tokens) or adjust the docs/behavior.

Copilot uses AI. Check for mistakes.
…ehalfOfParameterBuilder.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 26, 2026 15:38
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 26, 2026 15:46
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 8 comments.

/// <exception cref="ArgumentNullException">Thrown when <paramref name="attributeTokens"/> is null or contains no elements.</exception>
public AcquireTokenOnBehalfOfParameterBuilder WithAttributeTokens(IEnumerable<string> attributeTokens)
{
if (attributeTokens == null)
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Null checks in new code should use pattern matching (attributeTokens is null) per repo guidelines (avoid == null).

Suggested change
if (attributeTokens == null)
if (attributeTokens is null)

Copilot uses AI. Check for mistakes.
Comment on lines +171 to +180
public AcquireTokenForClientParameterBuilder WithAttributeTokens(IEnumerable<string> attributeTokens)
{
if (attributeTokens == null)
{
throw new ArgumentNullException(nameof(attributeTokens));
}

string joinedTokens = string.Join(" ", attributeTokens);

var extraBodyParams = new Dictionary<string, Func<CancellationToken, Task<string>>>
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WithAttributeTokens currently only checks for null, but the XML docs say it throws when the sequence is null or contains no elements. As implemented, an empty sequence results in an empty attribute_tokens value being sent (and tokens that are null/whitespace can produce extra spaces). Validate that there is at least one non-empty token (and trim/filter as needed) or update the XML docs to match the actual behavior.

Copilot uses AI. Check for mistakes.
/// <exception cref="ArgumentNullException">Thrown when <paramref name="attributeTokens"/> is null or contains no elements.</exception>
public AcquireTokenByAuthorizationCodeParameterBuilder WithAttributeTokens(IEnumerable<string> attributeTokens)
{
if (attributeTokens == null)
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The null check uses attributeTokens == null, but repo guidelines prefer pattern matching (attributeTokens is null) for null checks. Please update this (and any similar new checks) for consistency.

Suggested change
if (attributeTokens == null)
if (attributeTokens is null)

Copilot uses AI. Check for mistakes.

using System;
using System.Collections.Generic;
using System.Net.Http;
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new file includes using System.Net.Http; but all HttpMethod usage is fully-qualified (System.Net.Http.HttpMethod.Post), so the using is unused and will generate warnings in builds with analyzers enabled. Remove the unused using or use the imported type consistently.

Suggested change
using System.Net.Http;

Copilot uses AI. Check for mistakes.
Comment on lines +164 to +180
public AcquireTokenByAuthorizationCodeParameterBuilder WithAttributeTokens(IEnumerable<string> attributeTokens)
{
if (attributeTokens == null)
{
throw new ArgumentNullException(nameof(attributeTokens));
}

string joinedTokens = string.Join(" ", attributeTokens);

this.OnBeforeTokenRequest(async (data) =>
{
data.BodyParameters.Add(OAuth2Parameter.AttributeTokens, joinedTokens);
await Task.CompletedTask.ConfigureAwait(false);
});

return this;
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WithAttributeTokens adds attribute_tokens to the request body, but it does not add it to CommonParameters.CacheKeyComponents. Because MSAL filters cached access tokens by additional key components, tokens acquired with different attribute tokens can be mixed in cache. Add attribute_tokens as an additional cache key component (similar to other cache-key-sensitive parameters).

Copilot uses AI. Check for mistakes.
data.BodyParameters.Add(OAuth2Parameter.AttributeTokens, joinedTokens);
await Task.CompletedTask.ConfigureAwait(false);
});

Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like the auth-code builder, this adds attribute_tokens to the body but does not add it to CommonParameters.CacheKeyComponents. This can cause cached tokens for different attribute tokens to be treated as equivalent. Add attribute_tokens as an additional cache key component so cache lookups are correctly partitioned.

Suggested change
CommonParameters.CacheKeyComponents.Add(
new KeyValuePair<string, string>(OAuth2Parameter.AttributeTokens, joinedTokens));

Copilot uses AI. Check for mistakes.
Comment on lines +196 to +199
this.OnBeforeTokenRequest(async (data) =>
{
data.BodyParameters.Add(OAuth2Parameter.AttributeTokens, joinedTokens);
await Task.CompletedTask.ConfigureAwait(false);
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The OnBeforeTokenRequest handler is async but only awaits Task.CompletedTask, which is redundant. Return Task.CompletedTask directly to avoid an unnecessary async state machine allocation.

Suggested change
this.OnBeforeTokenRequest(async (data) =>
{
data.BodyParameters.Add(OAuth2Parameter.AttributeTokens, joinedTokens);
await Task.CompletedTask.ConfigureAwait(false);
this.OnBeforeTokenRequest((data) =>
{
data.BodyParameters.Add(OAuth2Parameter.AttributeTokens, joinedTokens);
return Task.CompletedTask;

Copilot uses AI. Check for mistakes.
Comment on lines +173 to +176
if (attributeTokens == null)
{
throw new ArgumentNullException(nameof(attributeTokens));
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Null checks in new code should use pattern matching (attributeTokens is null) per repo guidelines (avoid == null).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

};

this.WithAdditionalCacheKeyComponents(cacheKeyComponents);

Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WithAttributeTokens currently only adds attribute_tokens to CacheKeyComponents and never adds it to the token request body. As written, no attribute_tokens form parameter will be sent despite the XML doc stating it will. Consider using the same pattern as WithAttributes (e.g., hook OnBeforeTokenRequest / WithExtraBodyParameters) so the parameter is actually included in the POST body (and kept in the cache key).

Suggested change
// Ensure attribute_tokens is also sent as a form parameter in the token request body.
if (CommonParameters.ExtraBodyParameters == null)
{
CommonParameters.ExtraBodyParameters = new Dictionary<string, string>();
}
CommonParameters.ExtraBodyParameters[OAuth2Parameter.AttributeTokens] = joinedTokens;

Copilot uses AI. Check for mistakes.
Comment on lines +178 to +191
bool hasAnyToken = false;
foreach (var _ in attributeTokens)
{
hasAnyToken = true;
break;
}

if (!hasAnyToken)
{
throw new ArgumentNullException(nameof(attributeTokens));
}

string joinedTokens = string.Join(" ", attributeTokens);

Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method enumerates attributeTokens once to check for any items and then enumerates it again in string.Join. If the caller passes a single-use enumerable (or an expensive iterator), the join can produce incorrect results or incur unnecessary work. Buffer the tokens into a list/array once and validate/count based on that collection.

Copilot uses AI. Check for mistakes.
/// <param name="attributeTokens">A list of attribute token strings to include in the request.</param>
/// <returns>The builder to chain method calls.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="attributeTokens"/> is null or contains no elements.</exception>
public AcquireTokenByAuthorizationCodeParameterBuilder WithAttributeTokens(IEnumerable<string> attributeTokens)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be added to the base class ?

/// <exception cref="ArgumentNullException">Thrown when <paramref name="attributeTokens"/> is null or contains no elements.</exception>
public AcquireTokenForClientParameterBuilder WithAttributeTokens(IEnumerable<string> attributeTokens)
{
if (attributeTokens == null)
Copy link
Copy Markdown
Member

@bgavrilMS bgavrilMS Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a lot of duplicate code between these methods. In fact all 3 implemenations are different and 2 of them are wrong.

/// <exception cref="ArgumentNullException">Thrown when <paramref name="attributeTokens"/> is null or contains no elements.</exception>
public AcquireTokenForClientParameterBuilder WithAttributeTokens(IEnumerable<string> attributeTokens)
{
if (attributeTokens == null)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MSAL should be tollerant to null arguments. Just no-op. This provides a better dev ex.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated


this.OnBeforeTokenRequest((data) =>
{
data.BodyParameters.Add(OAuth2Parameter.AttributeTokens, joinedTokens);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will there be enough logging if we go with this approach?

Copy link
Copy Markdown
Member

@bgavrilMS bgavrilMS left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too much code duplication. No token caching.

Copilot AI review requested due to automatic review settings April 8, 2026 06:57
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 10 comments.

Comment on lines +1 to +5
Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithSendX5C(bool withSendX5C) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Microsoft.Identity.Client.IByUserFederatedIdentityCredential
Microsoft.Identity.Client.IByUserFederatedIdentityCredential.AcquireTokenByUserFederatedIdentityCredential(System.Collections.Generic.IEnumerable<string> scopes, string username, string assertion) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PublicAPI.Unshipped.txt includes API entries that already exist in PublicAPI.Shipped.txt (e.g., AcquireTokenByUserFederatedIdentityCredential*). Unshipped files should only contain newly introduced APIs; please remove the duplicated shipped entries and keep only the new WithAttributeTokens lines.

Suggested change
Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithSendX5C(bool withSendX5C) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Microsoft.Identity.Client.IByUserFederatedIdentityCredential
Microsoft.Identity.Client.IByUserFederatedIdentityCredential.AcquireTokenByUserFederatedIdentityCredential(System.Collections.Generic.IEnumerable<string> scopes, string username, string assertion) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +5
Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithSendX5C(bool withSendX5C) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Microsoft.Identity.Client.IByUserFederatedIdentityCredential
Microsoft.Identity.Client.IByUserFederatedIdentityCredential.AcquireTokenByUserFederatedIdentityCredential(System.Collections.Generic.IEnumerable<string> scopes, string username, string assertion) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PublicAPI.Unshipped.txt includes API entries that already exist in PublicAPI.Shipped.txt (e.g., AcquireTokenByUserFederatedIdentityCredential*). Unshipped files should only contain newly introduced APIs; please remove the duplicated shipped entries and keep only the new WithAttributeTokens lines.

Suggested change
Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithSendX5C(bool withSendX5C) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Microsoft.Identity.Client.IByUserFederatedIdentityCredential
Microsoft.Identity.Client.IByUserFederatedIdentityCredential.AcquireTokenByUserFederatedIdentityCredential(System.Collections.Generic.IEnumerable<string> scopes, string username, string assertion) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +5
Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithSendX5C(bool withSendX5C) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Microsoft.Identity.Client.IByUserFederatedIdentityCredential
Microsoft.Identity.Client.IByUserFederatedIdentityCredential.AcquireTokenByUserFederatedIdentityCredential(System.Collections.Generic.IEnumerable<string> scopes, string username, string assertion) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PublicAPI.Unshipped.txt includes API entries that already exist in PublicAPI.Shipped.txt (e.g., AcquireTokenByUserFederatedIdentityCredential*). Unshipped files should only contain newly introduced APIs; please remove the duplicated shipped entries and keep only the new WithAttributeTokens lines.

Suggested change
Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithSendX5C(bool withSendX5C) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Microsoft.Identity.Client.IByUserFederatedIdentityCredential
Microsoft.Identity.Client.IByUserFederatedIdentityCredential.AcquireTokenByUserFederatedIdentityCredential(System.Collections.Generic.IEnumerable<string> scopes, string username, string assertion) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +5
Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithSendX5C(bool withSendX5C) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Microsoft.Identity.Client.IByUserFederatedIdentityCredential
Microsoft.Identity.Client.IByUserFederatedIdentityCredential.AcquireTokenByUserFederatedIdentityCredential(System.Collections.Generic.IEnumerable<string> scopes, string username, string assertion) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PublicAPI.Unshipped.txt includes API entries that already exist in PublicAPI.Shipped.txt (e.g., AcquireTokenByUserFederatedIdentityCredential*). Unshipped files should only contain newly introduced APIs; please remove the duplicated shipped entries and keep only the new WithAttributeTokens lines.

Suggested change
Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithSendX5C(bool withSendX5C) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Microsoft.Identity.Client.IByUserFederatedIdentityCredential
Microsoft.Identity.Client.IByUserFederatedIdentityCredential.AcquireTokenByUserFederatedIdentityCredential(System.Collections.Generic.IEnumerable<string> scopes, string username, string assertion) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +5
Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithSendX5C(bool withSendX5C) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Microsoft.Identity.Client.IByUserFederatedIdentityCredential
Microsoft.Identity.Client.IByUserFederatedIdentityCredential.AcquireTokenByUserFederatedIdentityCredential(System.Collections.Generic.IEnumerable<string> scopes, string username, string assertion) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PublicAPI.Unshipped.txt includes API entries that already exist in PublicAPI.Shipped.txt (e.g., AcquireTokenByUserFederatedIdentityCredential*). Unshipped files should only contain newly introduced APIs; please remove the duplicated shipped entries and keep only the new WithAttributeTokens lines.

Suggested change
Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithSendX5C(bool withSendX5C) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder
Microsoft.Identity.Client.IByUserFederatedIdentityCredential
Microsoft.Identity.Client.IByUserFederatedIdentityCredential.AcquireTokenByUserFederatedIdentityCredential(System.Collections.Generic.IEnumerable<string> scopes, string username, string assertion) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder

Copilot uses AI. Check for mistakes.
Comment on lines +97 to +103
var extraBodyParams = new Dictionary<string, Func<CancellationToken, Task<string>>>
{
{ OAuth2Parameter.AttributeTokens, _ => Task.FromResult(joinedTokens) }
};

builder.WithExtraBodyParametersInternal(extraBodyParams);

Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WithAttributeTokensInternal adds attribute_tokens to WithAdditionalCacheKeyComponents(...). For user token flows like AcquireTokenByAuthorizationCode, this will cache access tokens under an extended cache key, but AcquireTokenSilent has no way to specify the same cache-key components. If a user ends up with both a “normal” token and an “attribute_tokens” token for the same account+scopes, silent cache lookup will see multiple candidates and throw MultipleTokensMatchedError. Consider either (1) adding a corresponding WithAttributeTokens API to AcquireTokenSilent (so callers can disambiguate) or (2) not including attribute_tokens in additional cache key components for flows that rely on AcquireTokenSilent.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copilot AI review requested due to automatic review settings April 8, 2026 12:16
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 6 comments.

Comment on lines +82 to +90
foreach (string token in attributeTokens)
{
if (!string.IsNullOrWhiteSpace(token))
{
string trimmed = token.Trim();
if (trimmed.IndexOfAny(new[] { ' ', '\t', '\n', '\r' }) >= 0)
{
throw new ArgumentException(
$"Attribute tokens must not contain whitespace. Invalid token: '{trimmed}'",
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ArgumentException message includes the (trimmed) attribute token value. Since these are called "tokens" they may be sensitive; embedding them in exception messages risks leaking secrets/PII via logs/telemetry. Prefer a generic message (optionally include the index/count) and avoid echoing the token value.

Suggested change
foreach (string token in attributeTokens)
{
if (!string.IsNullOrWhiteSpace(token))
{
string trimmed = token.Trim();
if (trimmed.IndexOfAny(new[] { ' ', '\t', '\n', '\r' }) >= 0)
{
throw new ArgumentException(
$"Attribute tokens must not contain whitespace. Invalid token: '{trimmed}'",
int tokenIndex = -1;
foreach (string token in attributeTokens)
{
tokenIndex++;
if (!string.IsNullOrWhiteSpace(token))
{
string trimmed = token.Trim();
if (trimmed.IndexOfAny(new[] { ' ', '\t', '\n', '\r' }) >= 0)
{
throw new ArgumentException(
$"Attribute tokens must not contain whitespace. Invalid token index: {tokenIndex}.",

Copilot uses AI. Check for mistakes.
if (!string.IsNullOrWhiteSpace(token))
{
string trimmed = token.Trim();
if (trimmed.IndexOfAny(new[] { ' ', '\t', '\n', '\r' }) >= 0)
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whitespace validation only checks for ' ', '\t', '\n', '\r'. This does not cover all Unicode whitespace characters (e.g., non‑breaking space), so tokens containing other whitespace will slip through despite the docs saying "must not contain whitespace". Consider using char.IsWhiteSpace-based validation to enforce the contract consistently.

Suggested change
if (trimmed.IndexOfAny(new[] { ' ', '\t', '\n', '\r' }) >= 0)
if (trimmed.Any(char.IsWhiteSpace))

Copilot uses AI. Check for mistakes.
Comment on lines +105 to +111
var extraBodyParams = new Dictionary<string, Func<CancellationToken, Task<string>>>
{
{ OAuth2Parameter.AttributeTokens, _ => Task.FromResult(joinedTokens) }
};

builder.WithExtraBodyParametersInternal(extraBodyParams);

Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WithAttributeTokensInternal adds attribute_tokens into CacheKeyComponents via WithExtraBodyParametersInternal, which stores access tokens with AdditionalCacheKeyComponents. Callers that later acquire tokens without specifying attribute tokens (e.g., AcquireTokenSilent, or an OBO call without WithAttributeTokens) will not set requestParams.CacheKeyComponents, so cache lookup will not filter by these components and can throw multiple_matching_tokens_detected if both token variants exist for the same account/scopes. To avoid this, either (1) add a corresponding WithAttributeTokens API to AcquireTokenSilentParameterBuilder (so callers can disambiguate), or (2) avoid partitioning user-flow tokens by attribute_tokens via additional cache key components and handle it another way (e.g., request-body-only, or a different cache strategy).

Suggested change
var extraBodyParams = new Dictionary<string, Func<CancellationToken, Task<string>>>
{
{ OAuth2Parameter.AttributeTokens, _ => Task.FromResult(joinedTokens) }
};
builder.WithExtraBodyParametersInternal(extraBodyParams);
builder.CommonParameters.ExtraBodyParameters ??= new Dictionary<string, Func<CancellationToken, Task<string>>>();
builder.CommonParameters.ExtraBodyParameters[OAuth2Parameter.AttributeTokens] = _ => Task.FromResult(joinedTokens);

Copilot uses AI. Check for mistakes.
{
ExpectedMethod = System.Net.Http.HttpMethod.Post,
ResponseMessage = MockHelpers.CreateSuccessTokenResponseMessage(),
ExpectedPostData = new Dictionary<string, string>()
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These "Null_NoOp" tests set ExpectedPostData to an empty dictionary, which doesn't assert that attribute_tokens is actually absent (the mock only checks presence of expected keys). Consider also setting UnExpectedPostData for OAuth2Parameter.AttributeTokens to ensure WithAttributeTokens(null) truly behaves as a no-op.

Suggested change
ExpectedPostData = new Dictionary<string, string>()
ExpectedPostData = new Dictionary<string, string>(),
UnExpectedPostData = new Dictionary<string, string>()
{
{ OAuth2Parameter.AttributeTokens, null }
}

Copilot uses AI. Check for mistakes.
.WithAttributeTokens(new[] { "valid", "invalid token", "another" }));

Assert.Contains("Attribute tokens must not contain whitespace", ex.Message);
Assert.Contains("invalid token", ex.Message);
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion expects the exception message to contain the invalid token value ("invalid token"). If the token value is sensitive, tests should not enforce echoing it in exception messages. Prefer asserting on a stable, non-sensitive part of the message (and/or the parameter name) instead.

Suggested change
Assert.Contains("invalid token", ex.Message);

Copilot uses AI. Check for mistakes.
{
ExpectedMethod = System.Net.Http.HttpMethod.Post,
ResponseMessage = MockHelpers.CreateSuccessTokenResponseMessage(),
ExpectedPostData = new Dictionary<string, string>()
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the OBO null test: using an empty ExpectedPostData doesn't verify that attribute_tokens is not sent. Add UnExpectedPostData for OAuth2Parameter.AttributeTokens (or otherwise assert absence) so the test fails if the parameter is accidentally included.

Suggested change
ExpectedPostData = new Dictionary<string, string>()
ExpectedPostData = new Dictionary<string, string>(),
UnExpectedPostData = new Dictionary<string, string>()
{
{ OAuth2Parameter.AttributeTokens, null }
}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants