Skip to content

Commit 3d12ae1

Browse files
Merge main into branch; resolve PublicAPI.Unshipped.txt conflicts
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2 parents 2f11fe9 + 5052e95 commit 3d12ae1

12 files changed

Lines changed: 1323 additions & 18 deletions

File tree

src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForClientParameterBuilder.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,8 @@ public AcquireTokenForClientParameterBuilder WithAttributes(string attributeJson
156156
{ OAuth2Parameter.Attributes, _ => Task.FromResult(attributeJson) }
157157
};
158158

159-
this.WithExtraBodyParameters(extraBodyParams);
159+
AbstractConfidentialClientAcquireTokenParameterBuilderExtension
160+
.WithExtraBodyParameters<AcquireTokenForClientParameterBuilder>(this, extraBodyParams);
160161

161162
return this;
162163
}

src/client/Microsoft.Identity.Client/Extensibility/AbstractConfidentialClientAcquireTokenParameterBuilderExtension.cs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Runtime.CompilerServices;
88
using System.Threading;
99
using System.Threading.Tasks;
10+
using Microsoft.Identity.Client.Core;
1011
using Microsoft.Identity.Client.OAuth2;
1112

1213
namespace Microsoft.Identity.Client.Extensibility
@@ -42,6 +43,132 @@ public static AbstractAcquireTokenParameterBuilder<T> OnBeforeTokenRequest<T>(
4243
return builder;
4344
}
4445

46+
/// <summary>
47+
/// Sends <paramref name="attributeTokens"/> as the <c>attribute_tokens</c> body parameter
48+
/// and includes them in the cache key. Null/empty/whitespace entries are ignored;
49+
/// a null or empty collection is a no-op.
50+
/// </summary>
51+
/// <typeparam name="T">The concrete confidential client builder type.</typeparam>
52+
/// <param name="builder">The builder to chain options to.</param>
53+
/// <param name="attributeTokens">Attribute tokens to include. Individual tokens must not contain whitespace.</param>
54+
/// <returns>The builder to chain method calls.</returns>
55+
/// <remarks>
56+
/// <para>
57+
/// For <c>AcquireTokenForClient</c> the <c>attribute_tokens</c> set is part of the cache
58+
/// partition key, so different sets are fully isolated.
59+
/// </para>
60+
/// <para>
61+
/// For <c>AcquireTokenOnBehalfOf</c> the partition key is the user assertion hash; the
62+
/// <c>attribute_tokens</c> set is stored as an item-level cache component within that
63+
/// partition. Multiple variants for the same assertion coexist and are disambiguated on
64+
/// read only when the caller supplies the same <c>WithAttributeTokens</c> set.
65+
/// <b>Mixing attributed and non-attributed reads against the same user assertion can
66+
/// return unintended cache entries or fail with <c>multiple_matching_tokens_detected</c>.</b>
67+
/// Be consistent: if you used <c>WithAttributeTokens</c> on a write, use it on every
68+
/// subsequent read for that assertion. Callers that require strict per-set cache isolation
69+
/// across different attribute-token sets should use separate <see cref="IConfidentialClientApplication"/>
70+
/// instances.
71+
/// </para>
72+
/// </remarks>
73+
/// <exception cref="ArgumentException">Thrown when any token contains embedded whitespace.</exception>
74+
/// <exception cref="MsalClientException">
75+
/// Thrown when the application was not configured to allow experimental features
76+
/// (this method transitively calls <see cref="WithExtraBodyParameters{T}"/>, which requires
77+
/// experimental features to be enabled via <c>WithExperimentalFeatures()</c> on the application builder).
78+
/// </exception>
79+
public static T WithAttributeTokens<T>(
80+
this AbstractConfidentialClientAcquireTokenParameterBuilder<T> builder,
81+
IEnumerable<string> attributeTokens)
82+
where T : AbstractConfidentialClientAcquireTokenParameterBuilder<T>
83+
{
84+
ILoggerAdapter logger = builder.ServiceBundle?.ApplicationLogger;
85+
86+
if (attributeTokens is null)
87+
{
88+
logger?.Verbose(() => "[WithAttributeTokens] No attribute tokens passed.");
89+
return (T)builder;
90+
}
91+
92+
var normalizedTokens = new List<string>();
93+
foreach (string token in attributeTokens)
94+
{
95+
if (!string.IsNullOrWhiteSpace(token))
96+
{
97+
string trimmed = token.Trim();
98+
if (trimmed.Any(char.IsWhiteSpace))
99+
{
100+
throw new ArgumentException(
101+
$"Attribute tokens must not contain whitespace. Invalid token: '{trimmed}'",
102+
nameof(attributeTokens));
103+
}
104+
105+
normalizedTokens.Add(trimmed);
106+
}
107+
}
108+
109+
if (normalizedTokens.Count == 0)
110+
{
111+
logger?.Verbose(() => "[WithAttributeTokens] collection contained no usable tokens.");
112+
return (T)builder;
113+
}
114+
115+
// Deduplicate and sort so that callers passing the same logical set of tokens
116+
// (in any order, with possible duplicates) hit the same cache entry and
117+
// produce the same request body.
118+
normalizedTokens = normalizedTokens.Distinct(StringComparer.Ordinal).ToList();
119+
normalizedTokens.Sort(StringComparer.Ordinal);
120+
121+
string joinedTokens = string.Join(" ", normalizedTokens);
122+
123+
int count = normalizedTokens.Count;
124+
logger?.Info(() => $"[WithAttributeTokens] Attaching {count} attribute token(s) " +
125+
"to the request body and including them in the cache key partition.");
126+
127+
var extraBodyParams = new Dictionary<string, Func<CancellationToken, Task<string>>>
128+
{
129+
{ OAuth2Parameter.AttributeTokens, _ => Task.FromResult(joinedTokens) }
130+
};
131+
132+
return builder.WithExtraBodyParameters(extraBodyParams);
133+
}
134+
135+
/// <summary>
136+
/// Add extra body parameters to the token request. These parameters are added to the cache key
137+
/// to associate these parameters with the acquired token. Works for confidential client flows
138+
/// (AcquireTokenForClient, AcquireTokenOnBehalfOf, AcquireTokenByAuthorizationCode).
139+
/// </summary>
140+
/// <typeparam name="T">The concrete confidential client builder type.</typeparam>
141+
/// <param name="builder">The builder to chain options to.</param>
142+
/// <param name="extraBodyParams">List of additional body parameters.</param>
143+
/// <returns>The concrete builder to chain method calls.</returns>
144+
public static T WithExtraBodyParameters<T>(
145+
this AbstractConfidentialClientAcquireTokenParameterBuilder<T> builder,
146+
Dictionary<string, Func<CancellationToken, Task<string>>> extraBodyParams)
147+
where T : AbstractConfidentialClientAcquireTokenParameterBuilder<T>
148+
{
149+
builder.ValidateUseOfExperimentalFeature();
150+
151+
if (extraBodyParams == null || extraBodyParams.Count == 0)
152+
{
153+
return (T)builder;
154+
}
155+
156+
builder.OnBeforeTokenRequest(async data =>
157+
{
158+
foreach (var param in extraBodyParams)
159+
{
160+
if (param.Value != null)
161+
{
162+
data.BodyParameters.Add(param.Key, await param.Value(data.CancellationToken).ConfigureAwait(false));
163+
}
164+
}
165+
});
166+
167+
builder.WithAdditionalCacheKeyComponents(extraBodyParams);
168+
169+
return (T)builder;
170+
}
171+
45172
/// <summary>
46173
/// Binds the token to a key in the cache.No cryptographic operations is performed on the token.
47174
/// </summary>

src/client/Microsoft.Identity.Client/Extensibility/AcquireTokenForClientBuilderExtensions.cs

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -49,27 +49,14 @@ public static AcquireTokenForClientParameterBuilder WithProofOfPosessionKeyId(
4949
/// <param name="builder"></param>
5050
/// <param name="extrabodyparams">List of additional body parameters</param>
5151
/// <returns></returns>
52+
[EditorBrowsable(EditorBrowsableState.Never)] // Soft deprecate; prefer the generic overload on AbstractConfidentialClientAcquireTokenParameterBuilderExtension that also supports OBO and AuthorizationCode.
53+
[Obsolete("Use AbstractConfidentialClientAcquireTokenParameterBuilderExtension.WithExtraBodyParameters instead, which also supports OBO and AuthorizationCode flows.", false)]
5254
public static AcquireTokenForClientParameterBuilder WithExtraBodyParameters(
5355
this AcquireTokenForClientParameterBuilder builder,
5456
Dictionary<string, Func<CancellationToken, Task<string>>> extrabodyparams)
5557
{
56-
builder.ValidateUseOfExperimentalFeature();
57-
if (extrabodyparams == null || extrabodyparams.Count == 0)
58-
{
59-
return builder;
60-
}
61-
builder.OnBeforeTokenRequest(async (data) =>
62-
{
63-
foreach (var param in extrabodyparams)
64-
{
65-
if (param.Value != null)
66-
{
67-
data.BodyParameters.Add(param.Key, await param.Value(data.CancellationToken).ConfigureAwait(false));
68-
}
69-
}
70-
});
71-
72-
builder.WithAdditionalCacheKeyComponents(extrabodyparams);
58+
AbstractConfidentialClientAcquireTokenParameterBuilderExtension
59+
.WithExtraBodyParameters<AcquireTokenForClientParameterBuilder>(builder, extrabodyparams);
7360
return builder;
7461
}
7562
}

src/client/Microsoft.Identity.Client/OAuth2/OAuthConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ internal static class OAuth2Parameter
4646
public const string SpaCode = "return_spa_code"; // not a standard OAuth2 param
4747
public const string FmiPath = "fmi_path"; // not a standard OAuth2 param
4848
public const string Attributes = "attributes"; // not a standard OAuth2 param
49+
public const string AttributeTokens = "attribute_tokens"; // not a standard OAuth2 param
4950
public const string UserFederatedIdentityCredential = "user_federated_identity_credential"; // user_fic grant type parameter
5051
}
5152

src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@ Microsoft.Identity.Client.Extensibility.AuthenticationResultExtensions
99
const Microsoft.Identity.Client.MsalError.InternalCacheDisabled = "internal_cache_disabled" -> string
1010
const Microsoft.Identity.Client.MsalError.InvalidCredentialMaterial = "invalid_credential_material" -> string
1111
static Microsoft.Identity.Client.CacheOptions.DisableInternalCacheOptions.get -> Microsoft.Identity.Client.CacheOptions
12+
static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquireTokenParameterBuilderExtension.WithAttributeTokens<T>(this Microsoft.Identity.Client.AbstractConfidentialClientAcquireTokenParameterBuilder<T> builder, System.Collections.Generic.IEnumerable<string> attributeTokens) -> T
13+
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
1214
static Microsoft.Identity.Client.Extensibility.AuthenticationResultExtensions.GetRefreshToken(this Microsoft.Identity.Client.AuthenticationResult result) -> string

src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@ Microsoft.Identity.Client.Extensibility.AuthenticationResultExtensions
99
const Microsoft.Identity.Client.MsalError.InternalCacheDisabled = "internal_cache_disabled" -> string
1010
const Microsoft.Identity.Client.MsalError.InvalidCredentialMaterial = "invalid_credential_material" -> string
1111
static Microsoft.Identity.Client.CacheOptions.DisableInternalCacheOptions.get -> Microsoft.Identity.Client.CacheOptions
12+
static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquireTokenParameterBuilderExtension.WithAttributeTokens<T>(this Microsoft.Identity.Client.AbstractConfidentialClientAcquireTokenParameterBuilder<T> builder, System.Collections.Generic.IEnumerable<string> attributeTokens) -> T
13+
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
1214
static Microsoft.Identity.Client.Extensibility.AuthenticationResultExtensions.GetRefreshToken(this Microsoft.Identity.Client.AuthenticationResult result) -> string

src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@ Microsoft.Identity.Client.Extensibility.AuthenticationResultExtensions
99
const Microsoft.Identity.Client.MsalError.InternalCacheDisabled = "internal_cache_disabled" -> string
1010
const Microsoft.Identity.Client.MsalError.InvalidCredentialMaterial = "invalid_credential_material" -> string
1111
static Microsoft.Identity.Client.CacheOptions.DisableInternalCacheOptions.get -> Microsoft.Identity.Client.CacheOptions
12+
static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquireTokenParameterBuilderExtension.WithAttributeTokens<T>(this Microsoft.Identity.Client.AbstractConfidentialClientAcquireTokenParameterBuilder<T> builder, System.Collections.Generic.IEnumerable<string> attributeTokens) -> T
13+
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
1214
static Microsoft.Identity.Client.Extensibility.AuthenticationResultExtensions.GetRefreshToken(this Microsoft.Identity.Client.AuthenticationResult result) -> string

src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@ Microsoft.Identity.Client.Extensibility.AuthenticationResultExtensions
99
const Microsoft.Identity.Client.MsalError.InternalCacheDisabled = "internal_cache_disabled" -> string
1010
const Microsoft.Identity.Client.MsalError.InvalidCredentialMaterial = "invalid_credential_material" -> string
1111
static Microsoft.Identity.Client.CacheOptions.DisableInternalCacheOptions.get -> Microsoft.Identity.Client.CacheOptions
12+
static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquireTokenParameterBuilderExtension.WithAttributeTokens<T>(this Microsoft.Identity.Client.AbstractConfidentialClientAcquireTokenParameterBuilder<T> builder, System.Collections.Generic.IEnumerable<string> attributeTokens) -> T
13+
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
1214
static Microsoft.Identity.Client.Extensibility.AuthenticationResultExtensions.GetRefreshToken(this Microsoft.Identity.Client.AuthenticationResult result) -> string

src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@ Microsoft.Identity.Client.Extensibility.AuthenticationResultExtensions
99
const Microsoft.Identity.Client.MsalError.InternalCacheDisabled = "internal_cache_disabled" -> string
1010
const Microsoft.Identity.Client.MsalError.InvalidCredentialMaterial = "invalid_credential_material" -> string
1111
static Microsoft.Identity.Client.CacheOptions.DisableInternalCacheOptions.get -> Microsoft.Identity.Client.CacheOptions
12+
static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquireTokenParameterBuilderExtension.WithAttributeTokens<T>(this Microsoft.Identity.Client.AbstractConfidentialClientAcquireTokenParameterBuilder<T> builder, System.Collections.Generic.IEnumerable<string> attributeTokens) -> T
13+
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
1214
static Microsoft.Identity.Client.Extensibility.AuthenticationResultExtensions.GetRefreshToken(this Microsoft.Identity.Client.AuthenticationResult result) -> string

src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@ Microsoft.Identity.Client.Extensibility.AuthenticationResultExtensions
99
const Microsoft.Identity.Client.MsalError.InternalCacheDisabled = "internal_cache_disabled" -> string
1010
const Microsoft.Identity.Client.MsalError.InvalidCredentialMaterial = "invalid_credential_material" -> string
1111
static Microsoft.Identity.Client.CacheOptions.DisableInternalCacheOptions.get -> Microsoft.Identity.Client.CacheOptions
12+
static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquireTokenParameterBuilderExtension.WithAttributeTokens<T>(this Microsoft.Identity.Client.AbstractConfidentialClientAcquireTokenParameterBuilder<T> builder, System.Collections.Generic.IEnumerable<string> attributeTokens) -> T
13+
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
1214
static Microsoft.Identity.Client.Extensibility.AuthenticationResultExtensions.GetRefreshToken(this Microsoft.Identity.Client.AuthenticationResult result) -> string

0 commit comments

Comments
 (0)