Conversation
There was a problem hiding this comment.
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.AttributeTokensconstant ("attribute_tokens"). - Updated PublicAPI
Unshippedbaselines 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. |
| [TestMethod] | ||
| public async Task WithAttributeTokens_OnBehalfOf_AddsSpaceSeparatedTokensToBody_Async() | ||
| { | ||
| using (var httpManager = new MockHttpManager()) | ||
| { |
There was a problem hiding this comment.
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).
| string joinedTokens = string.Join(" ", attributeTokens); | ||
|
|
||
| this.OnBeforeTokenRequest(async (data) => | ||
| { | ||
| data.BodyParameters.Add(OAuth2Parameter.AttributeTokens, joinedTokens); |
There was a problem hiding this comment.
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).
src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenOnBehalfOfParameterBuilder.cs
Outdated
Show resolved
Hide resolved
src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenOnBehalfOfParameterBuilder.cs
Outdated
Show resolved
Hide resolved
| /// <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); | ||
|
|
There was a problem hiding this comment.
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.
...lient/Microsoft.Identity.Client/ApiConfig/AcquireTokenByAuthorizationCodeParameterBuilder.cs
Outdated
Show resolved
Hide resolved
| /// <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); |
There was a problem hiding this comment.
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.
src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForClientParameterBuilder.cs
Outdated
Show resolved
Hide resolved
…ehalfOfParameterBuilder.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
| /// <exception cref="ArgumentNullException">Thrown when <paramref name="attributeTokens"/> is null or contains no elements.</exception> | ||
| public AcquireTokenOnBehalfOfParameterBuilder WithAttributeTokens(IEnumerable<string> attributeTokens) | ||
| { | ||
| if (attributeTokens == null) |
There was a problem hiding this comment.
Null checks in new code should use pattern matching (attributeTokens is null) per repo guidelines (avoid == null).
| if (attributeTokens == null) | |
| if (attributeTokens is null) |
| 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>>> |
There was a problem hiding this comment.
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.
| /// <exception cref="ArgumentNullException">Thrown when <paramref name="attributeTokens"/> is null or contains no elements.</exception> | ||
| public AcquireTokenByAuthorizationCodeParameterBuilder WithAttributeTokens(IEnumerable<string> attributeTokens) | ||
| { | ||
| if (attributeTokens == null) |
There was a problem hiding this comment.
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.
| if (attributeTokens == null) | |
| if (attributeTokens is null) |
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Net.Http; |
There was a problem hiding this comment.
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.
| using System.Net.Http; |
| 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; | ||
| } |
There was a problem hiding this comment.
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).
| data.BodyParameters.Add(OAuth2Parameter.AttributeTokens, joinedTokens); | ||
| await Task.CompletedTask.ConfigureAwait(false); | ||
| }); | ||
|
|
There was a problem hiding this comment.
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.
| CommonParameters.CacheKeyComponents.Add( | |
| new KeyValuePair<string, string>(OAuth2Parameter.AttributeTokens, joinedTokens)); |
| this.OnBeforeTokenRequest(async (data) => | ||
| { | ||
| data.BodyParameters.Add(OAuth2Parameter.AttributeTokens, joinedTokens); | ||
| await Task.CompletedTask.ConfigureAwait(false); |
There was a problem hiding this comment.
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.
| 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; |
| if (attributeTokens == null) | ||
| { | ||
| throw new ArgumentNullException(nameof(attributeTokens)); | ||
| } |
There was a problem hiding this comment.
Null checks in new code should use pattern matching (attributeTokens is null) per repo guidelines (avoid == null).
| }; | ||
|
|
||
| this.WithAdditionalCacheKeyComponents(cacheKeyComponents); | ||
|
|
There was a problem hiding this comment.
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).
| // 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; |
| bool hasAnyToken = false; | ||
| foreach (var _ in attributeTokens) | ||
| { | ||
| hasAnyToken = true; | ||
| break; | ||
| } | ||
|
|
||
| if (!hasAnyToken) | ||
| { | ||
| throw new ArgumentNullException(nameof(attributeTokens)); | ||
| } | ||
|
|
||
| string joinedTokens = string.Join(" ", attributeTokens); | ||
|
|
There was a problem hiding this comment.
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.
| /// <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) |
There was a problem hiding this comment.
Should this be added to the base class ?
...lient/Microsoft.Identity.Client/ApiConfig/AcquireTokenByAuthorizationCodeParameterBuilder.cs
Outdated
Show resolved
Hide resolved
| /// <exception cref="ArgumentNullException">Thrown when <paramref name="attributeTokens"/> is null or contains no elements.</exception> | ||
| public AcquireTokenForClientParameterBuilder WithAttributeTokens(IEnumerable<string> attributeTokens) | ||
| { | ||
| if (attributeTokens == null) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
MSAL should be tollerant to null arguments. Just no-op. This provides a better dev ex.
|
|
||
| this.OnBeforeTokenRequest((data) => | ||
| { | ||
| data.BodyParameters.Add(OAuth2Parameter.AttributeTokens, joinedTokens); |
There was a problem hiding this comment.
Will there be enough logging if we go with this approach?
bgavrilMS
left a comment
There was a problem hiding this comment.
Too much code duplication. No token caching.
| 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 |
There was a problem hiding this comment.
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.
| 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 |
| 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 |
There was a problem hiding this comment.
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.
| 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 |
| 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 |
There was a problem hiding this comment.
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.
| 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 |
| 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 |
There was a problem hiding this comment.
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.
| 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 |
| 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 |
There was a problem hiding this comment.
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.
| 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 |
src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt
Outdated
Show resolved
Hide resolved
...tity.Client/Extensibility/AbstractConfidentialClientAcquireTokenParameterBuilderExtension.cs
Show resolved
Hide resolved
src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenOnBehalfOfParameterBuilder.cs
Outdated
Show resolved
Hide resolved
...lient/Microsoft.Identity.Client/ApiConfig/AcquireTokenByAuthorizationCodeParameterBuilder.cs
Outdated
Show resolved
Hide resolved
| var extraBodyParams = new Dictionary<string, Func<CancellationToken, Task<string>>> | ||
| { | ||
| { OAuth2Parameter.AttributeTokens, _ => Task.FromResult(joinedTokens) } | ||
| }; | ||
|
|
||
| builder.WithExtraBodyParametersInternal(extraBodyParams); | ||
|
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@copilot apply changes based on this feedback
| 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}'", |
There was a problem hiding this comment.
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.
| 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}.", |
| if (!string.IsNullOrWhiteSpace(token)) | ||
| { | ||
| string trimmed = token.Trim(); | ||
| if (trimmed.IndexOfAny(new[] { ' ', '\t', '\n', '\r' }) >= 0) |
There was a problem hiding this comment.
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.
| if (trimmed.IndexOfAny(new[] { ' ', '\t', '\n', '\r' }) >= 0) | |
| if (trimmed.Any(char.IsWhiteSpace)) |
| var extraBodyParams = new Dictionary<string, Func<CancellationToken, Task<string>>> | ||
| { | ||
| { OAuth2Parameter.AttributeTokens, _ => Task.FromResult(joinedTokens) } | ||
| }; | ||
|
|
||
| builder.WithExtraBodyParametersInternal(extraBodyParams); | ||
|
|
There was a problem hiding this comment.
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).
| 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); |
| { | ||
| ExpectedMethod = System.Net.Http.HttpMethod.Post, | ||
| ResponseMessage = MockHelpers.CreateSuccessTokenResponseMessage(), | ||
| ExpectedPostData = new Dictionary<string, string>() |
There was a problem hiding this comment.
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.
| ExpectedPostData = new Dictionary<string, string>() | |
| ExpectedPostData = new Dictionary<string, string>(), | |
| UnExpectedPostData = new Dictionary<string, string>() | |
| { | |
| { OAuth2Parameter.AttributeTokens, null } | |
| } |
| .WithAttributeTokens(new[] { "valid", "invalid token", "another" })); | ||
|
|
||
| Assert.Contains("Attribute tokens must not contain whitespace", ex.Message); | ||
| Assert.Contains("invalid token", ex.Message); |
There was a problem hiding this comment.
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.
| Assert.Contains("invalid token", ex.Message); |
| { | ||
| ExpectedMethod = System.Net.Http.HttpMethod.Post, | ||
| ResponseMessage = MockHelpers.CreateSuccessTokenResponseMessage(), | ||
| ExpectedPostData = new Dictionary<string, string>() |
There was a problem hiding this comment.
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.
| ExpectedPostData = new Dictionary<string, string>() | |
| ExpectedPostData = new Dictionary<string, string>(), | |
| UnExpectedPostData = new Dictionary<string, string>() | |
| { | |
| { OAuth2Parameter.AttributeTokens, null } | |
| } |
Added support for
WithAttributeTokensfor CCA acquisition methodsThis 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
WithAttributeTokensmethods to the parameter builders for authorization code, client credentials, and on-behalf-of flows, allowing developers to include custom attribute tokens in requests via theattribute_tokensparameter.The most important changes include:
New API Surface:
Added
WithAttributeTokens(IEnumerable<string> attributeTokens)methods toAcquireTokenByAuthorizationCodeParameterBuilder,AcquireTokenForClientParameterBuilder, andAcquireTokenOnBehalfOfParameterBuilder, 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
WithAttributeTokensmethods, ensuring they are part of the public API surface.OAuth2 Parameter Support:
AttributeTokensto theOAuth2Parameterclass for use as the key in the request body parameter.Fixes #
Testing
Unit test added.