Skip to content

Commit 3ea20b4

Browse files
trwalketrwalke
andauthored
Updating MSAL to send client info = 2 on client credential flow (#5529)
* Updating MSAL to send client info = 2 cor client credential flow * Updating xmsacb handling. added test * Fixing error * Updating xms_acb implementation * Updating tests * Making implementation more generic * Update test * Refactoring * Updating test * Resolving null ref * Updating test data * Fixing test errors * Fixing build * Fixed test errors --------- Co-authored-by: trwalke <[email protected]>
1 parent 2d65ccd commit 3ea20b4

File tree

12 files changed

+203
-17
lines changed

12 files changed

+203
-17
lines changed

src/client/Microsoft.Identity.Client/AuthenticationResult.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,11 @@ internal AuthenticationResult(
167167
CorrelationId = correlationID;
168168
ApiEvent = apiEvent;
169169
AuthenticationResultMetadata = new AuthenticationResultMetadata(tokenSource);
170+
170171
AdditionalResponseParameters = msalAccessTokenCacheItem?.PersistedCacheParameters?.Count > 0 ?
171172
(IReadOnlyDictionary<string, string>)msalAccessTokenCacheItem.PersistedCacheParameters :
172173
additionalResponseParameters;
174+
173175
if (msalAccessTokenCacheItem != null)
174176
{
175177
ExpiresOn = msalAccessTokenCacheItem.ExpiresOn;

src/client/Microsoft.Identity.Client/Cache/Items/MsalAccessTokenCacheItem.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,25 @@ private IDictionary<string, string> AcquireCacheParametersFromResponse(
8888
#endif
8989
return cacheParameters;
9090
}
91+
92+
internal void AddAdditionalCacheParameters(Dictionary<string, string> additionalCacheParameters)
93+
{
94+
if (additionalCacheParameters != null)
95+
{
96+
if (PersistedCacheParameters == null)
97+
{
98+
PersistedCacheParameters = new Dictionary<string, string>(additionalCacheParameters);
99+
}
100+
else
101+
{
102+
foreach (var kvp in additionalCacheParameters)
103+
{
104+
PersistedCacheParameters[kvp.Key] = kvp.Value;
105+
}
106+
}
107+
}
108+
}
109+
91110
#endif
92111
internal /* for test */ MsalAccessTokenCacheItem(
93112
string preferredCacheEnv,

src/client/Microsoft.Identity.Client/Internal/ClientInfo.cs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
using System;
5+
using System.Collections.Generic;
56
using System.Globalization;
67
using Microsoft.Identity.Client.Utils;
78
#if SUPPORTS_SYSTEM_TEXT_JSON
@@ -13,7 +14,6 @@
1314

1415
namespace Microsoft.Identity.Client.Internal
1516
{
16-
1717
[JsonObject]
1818
[Preserve(AllMembers = true)]
1919
internal class ClientInfo
@@ -24,6 +24,8 @@ internal class ClientInfo
2424
[JsonProperty(ClientInfoClaim.UniqueTenantIdentifier)]
2525
public string UniqueTenantIdentifier { get; set; }
2626

27+
public Dictionary<string, string> AdditionalResponseParameters { get; private set; }
28+
2729
public static ClientInfo CreateFromJson(string clientInfo)
2830
{
2931
if (string.IsNullOrEmpty(clientInfo))
@@ -35,7 +37,34 @@ public static ClientInfo CreateFromJson(string clientInfo)
3537

3638
try
3739
{
38-
return JsonHelper.DeserializeFromJson<ClientInfo>(Base64UrlHelpers.DecodeBytes(clientInfo));
40+
var decodedBytes = Base64UrlHelpers.DecodeBytes(clientInfo);
41+
42+
// Deserialize into a dictionary to get all properties
43+
var allProperties = JsonHelper.DeserializeFromJson<Dictionary<string, object>>(decodedBytes);
44+
45+
var clientInfoObj = new ClientInfo();
46+
var additionalParams = new Dictionary<string, string>();
47+
48+
// Extract known claims and store the rest in AdditionalResponseParameters
49+
foreach (var kvp in allProperties)
50+
{
51+
if (kvp.Key == ClientInfoClaim.UniqueIdentifier)
52+
{
53+
clientInfoObj.UniqueObjectIdentifier = kvp.Value?.ToString();
54+
55+
}
56+
else if (kvp.Key == ClientInfoClaim.UniqueTenantIdentifier)
57+
{
58+
clientInfoObj.UniqueTenantIdentifier = kvp.Value?.ToString();
59+
}
60+
else
61+
{
62+
additionalParams[kvp.Key] = kvp.Value?.ToString() ?? string.Empty;
63+
}
64+
}
65+
66+
clientInfoObj.AdditionalResponseParameters = additionalParams;
67+
return clientInfoObj;
3968
}
4069
catch (Exception exc)
4170
{

src/client/Microsoft.Identity.Client/Internal/Requests/ClientCredentialRequest.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,8 @@ private Dictionary<string, string> GetBodyParameters()
335335
var dict = new Dictionary<string, string>
336336
{
337337
[OAuth2Parameter.GrantType] = OAuth2GrantType.ClientCredentials,
338-
[OAuth2Parameter.Scope] = AuthenticationRequestParameters.Scope.AsSingleString()
338+
[OAuth2Parameter.Scope] = AuthenticationRequestParameters.Scope.AsSingleString(),
339+
[OAuth2Parameter.ClientInfo] = "2"
339340
};
340341

341342
return dict;

src/client/Microsoft.Identity.Client/Internal/Requests/RequestBase.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -317,28 +317,28 @@ protected async Task<AuthenticationResult> CacheTokenResponseAndCreateAuthentica
317317
// developer passed in user object.
318318
AuthenticationRequestParameters.RequestContext.Logger.Info("Checking client info returned from the server..");
319319

320-
ClientInfo fromServer = null;
320+
ClientInfo clientInfoFromServer = null;
321321

322-
if (!AuthenticationRequestParameters.IsClientCredentialRequest &&
323-
AuthenticationRequestParameters.ApiId != ApiEvent.ApiIds.AcquireTokenForSystemAssignedManagedIdentity &&
322+
if (AuthenticationRequestParameters.ApiId != ApiEvent.ApiIds.AcquireTokenForSystemAssignedManagedIdentity &&
324323
AuthenticationRequestParameters.ApiId != ApiEvent.ApiIds.AcquireTokenForUserAssignedManagedIdentity &&
325324
AuthenticationRequestParameters.ApiId != ApiEvent.ApiIds.AcquireTokenByRefreshToken &&
326325
AuthenticationRequestParameters.AuthorityInfo.AuthorityType != AuthorityType.Adfs &&
327326
!(msalTokenResponse.ClientInfo is null))
328327
{
329-
//client_info is not returned from client credential and managed identity flows because there is no user present.
330-
fromServer = ClientInfo.CreateFromJson(msalTokenResponse.ClientInfo);
328+
//client_info is not returned from managed identity flows because there is no user present.
329+
clientInfoFromServer = ClientInfo.CreateFromJson(msalTokenResponse.ClientInfo);
330+
ValidateAccountIdentifiers(clientInfoFromServer);
331331
}
332332

333-
ValidateAccountIdentifiers(fromServer);
334-
335333
AuthenticationRequestParameters.RequestContext.Logger.Info("Saving token response to cache..");
336334

337335
var tuple = await CacheManager.SaveTokenResponseAsync(msalTokenResponse).ConfigureAwait(false);
338336
var atItem = tuple.Item1;
339337
var idtItem = tuple.Item2;
340338
Account account = tuple.Item3;
341-
339+
#if !MOBILE
340+
atItem?.AddAdditionalCacheParameters(clientInfoFromServer?.AdditionalResponseParameters);
341+
#endif
342342
return new AuthenticationResult(
343343
atItem,
344344
idtItem,

src/client/Microsoft.Identity.Client/Platforms/net/JsonStringConverter.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,17 @@ public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOp
3838
{
3939
writer.WriteStringValue(value);
4040
}
41+
42+
//Provides Support for reading dictionary key strings
43+
public override string ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
44+
{
45+
return reader.GetString();
46+
}
47+
48+
//Provides Support for writing dictionary key strings
49+
public override void WriteAsPropertyName(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
50+
{
51+
writer.WritePropertyName(value);
52+
}
4153
}
4254
}

src/client/Microsoft.Identity.Client/Platforms/net/MsalJsonSerializerContext.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ namespace Microsoft.Identity.Client.Platforms.net
4242
[JsonSerializable(typeof(CuidInfo))]
4343
[JsonSerializable(typeof(CertificateRequestBody))]
4444
[JsonSerializable(typeof(CertificateRequestResponse))]
45+
[JsonSerializable(typeof(Dictionary<string, object>))]
4546
[JsonSourceGenerationOptions]
4647
internal partial class MsalJsonSerializerContext : JsonSerializerContext
4748
{

tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHelpers.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,13 @@ public static string GetMsiImdsErrorResponse()
184184
"\"correlation_id\":\"77145480-bc5a-4ebe-ae4d-e4a8b7d727cf\",\"error_uri\":\"https://westus2.login.microsoft.com/error?code=500011\"}";
185185
}
186186

187-
public static string CreateClientInfo(string uid = TestConstants.Uid, string utid = TestConstants.Utid)
187+
public static string CreateClientInfo(string uid = TestConstants.Uid, string utid = TestConstants.Utid, bool CreateClientInfoForS2S = false)
188188
{
189+
if (CreateClientInfoForS2S)
190+
{
191+
return Base64UrlHelpers.Encode("{\"authz\":[\"value1\",\"value2\"]}");
192+
}
193+
189194
return Base64UrlHelpers.Encode("{\"uid\":\"" + uid + "\",\"utid\":\"" + utid + "\"}");
190195
}
191196

@@ -368,6 +373,17 @@ public static HttpResponseMessage CreateSuccessfulClientCredentialTokenResponseM
368373
"{\"token_type\":\"" + tokenType + "\",\"expires_in\":\"" + expiry + "\",\"access_token\":\"" + token + "\",\"additional_param1\":\"value1\",\"additional_param2\":\"value2\",\"additional_param3\":\"value3\"}");
369374
}
370375

376+
public static HttpResponseMessage CreateSuccessfulClientCredentialTokenResponseWithClientInfoMessage(
377+
string token = "header.payload.signature",
378+
string expiry = "3599",
379+
string tokenType = "Bearer",
380+
bool CreateClientInfoForS2S = false
381+
)
382+
{
383+
return CreateSuccessResponseMessage(
384+
"{\"token_type\":\"" + tokenType + "\",\"expires_in\":\"" + expiry + "\",\"access_token\":\"" + token + "\",\"additional_param1\":\"value1\",\"additional_param2\":\"value2\",\"additional_param3\":\"value3\",\"client_info\":\"" + CreateClientInfo(null, null, CreateClientInfoForS2S) + "\"}");
385+
}
386+
371387
public static HttpResponseMessage CreateSuccessfulClientCredentialTokenResponseWithAdditionalParamsMessage(
372388
string token = "header.payload.signature",
373389
string expiry = "3599",

tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManagerExtensions.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -183,18 +183,20 @@ public static void AddMockHandlerContentNotFound(this MockHttpManager httpManage
183183
}
184184

185185
public static MockHttpMessageHandler AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(
186-
this MockHttpManager httpManager,
187-
string token = "header.payload.signature",
186+
this MockHttpManager httpManager,
187+
string token = "header.payload.signature",
188188
string expiresIn = "3599",
189189
string tokenType = "Bearer",
190190
IList<string> unexpectedHttpHeaders = null,
191-
Dictionary<string, string> expectedPostData = null
191+
Dictionary<string, string> expectedPostData = null,
192+
bool addClientInfo = false
192193
)
193194
{
194195
var handler = new MockHttpMessageHandler()
195196
{
196197
ExpectedMethod = HttpMethod.Post,
197-
ResponseMessage = MockHelpers.CreateSuccessfulClientCredentialTokenResponseMessage(token, expiresIn, tokenType),
198+
ResponseMessage = addClientInfo? MockHelpers.CreateSuccessfulClientCredentialTokenResponseWithClientInfoMessage(token, expiresIn, tokenType, true)
199+
: MockHelpers.CreateSuccessfulClientCredentialTokenResponseMessage(token, expiresIn, tokenType),
198200
UnexpectedRequestHeaders = unexpectedHttpHeaders,
199201
ExpectedPostData = expectedPostData
200202
};

tests/Microsoft.Identity.Test.Unit/PublicApiTests/ClientCredentialWithCertTest.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
using static Microsoft.Identity.Client.Internal.JsonWebToken;
2424
using Microsoft.Identity.Client.RP;
2525
using Microsoft.Identity.Client.Http;
26+
using Microsoft.Identity.Client.OAuth2;
2627

2728
namespace Microsoft.Identity.Test.Unit
2829
{
@@ -1019,6 +1020,43 @@ public void EnsureNullCertDoesNotSetSerialNumberTestAsync()
10191020
}
10201021
}
10211022

1023+
[TestMethod]
1024+
public async Task AcquireTokenForClient_ShouldSendClientInfoParameter_WithValueTwo_Async()
1025+
{
1026+
// Arrange
1027+
using (var httpManager = new MockHttpManager())
1028+
{
1029+
httpManager.AddInstanceDiscoveryMockHandler();
1030+
1031+
// Set up the expected POST data to include client_info = "2"
1032+
var expectedPostData = new Dictionary<string, string>
1033+
{
1034+
[OAuth2Parameter.GrantType] = OAuth2GrantType.ClientCredentials,
1035+
[OAuth2Parameter.Scope] = TestConstants.s_scope.AsSingleString(),
1036+
[OAuth2Parameter.ClientInfo] = "2"
1037+
};
1038+
1039+
var handler = httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(
1040+
expectedPostData: expectedPostData);
1041+
1042+
var app = ConfidentialClientApplicationBuilder
1043+
.Create(TestConstants.ClientId)
1044+
.WithClientSecret(TestConstants.ClientSecret)
1045+
.WithAuthority(TestConstants.AuthorityCommonTenant)
1046+
.WithHttpManager(httpManager)
1047+
.BuildConcrete();
1048+
1049+
// Act
1050+
var result = await app.AcquireTokenForClient(TestConstants.s_scope)
1051+
.ExecuteAsync()
1052+
.ConfigureAwait(false);
1053+
1054+
// Assert
1055+
Assert.IsNotNull(result);
1056+
Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource);
1057+
}
1058+
}
1059+
10221060
private void BeforeCacheAccess(TokenCacheNotificationArgs args)
10231061
{
10241062
args.TokenCache.DeserializeMsalV3(_serializedCache);

0 commit comments

Comments
 (0)