Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Microsoft.Identity.Client.TelemetryCore.Internal.Events;
using Microsoft.Identity.Client.Utils;
using System.Security.Cryptography.X509Certificates;
using System.Linq;

namespace Microsoft.Identity.Client
{
Expand Down Expand Up @@ -167,9 +168,11 @@ internal AuthenticationResult(
CorrelationId = correlationID;
ApiEvent = apiEvent;
AuthenticationResultMetadata = new AuthenticationResultMetadata(tokenSource);

AdditionalResponseParameters = msalAccessTokenCacheItem?.PersistedCacheParameters?.Count > 0 ?
(IReadOnlyDictionary<string, string>)msalAccessTokenCacheItem.PersistedCacheParameters :
additionalResponseParameters;

if (msalAccessTokenCacheItem != null)
{
ExpiresOn = msalAccessTokenCacheItem.ExpiresOn;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,25 @@ private IDictionary<string, string> AcquireCacheParametersFromResponse(
#endif
return cacheParameters;
}

internal void AddPersistedCacheParameters(Dictionary<string, string> additionalPersistedCacheParameters)
{
if (additionalPersistedCacheParameters != null)
{
if (PersistedCacheParameters == null)
{
PersistedCacheParameters = new Dictionary<string, string>(additionalPersistedCacheParameters);
}
else
{
foreach (var kvp in additionalPersistedCacheParameters)
{
PersistedCacheParameters[kvp.Key] = kvp.Value;
}
}
}
}

#endif
internal /* for test */ MsalAccessTokenCacheItem(
string preferredCacheEnv,
Expand Down Expand Up @@ -288,6 +307,8 @@ internal string TenantId
/// </summary>
internal IDictionary<string, string> PersistedCacheParameters { get; private set; }

internal string AcbAuthN { get; private set; }

private Lazy<IiOSKey> iOSCacheKeyLazy;
public IiOSKey iOSCacheKey => iOSCacheKeyLazy.Value;

Expand Down
33 changes: 31 additions & 2 deletions src/client/Microsoft.Identity.Client/Internal/ClientInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Globalization;
using Microsoft.Identity.Client.Utils;
#if SUPPORTS_SYSTEM_TEXT_JSON
Expand All @@ -13,7 +14,6 @@

namespace Microsoft.Identity.Client.Internal
{

[JsonObject]
[Preserve(AllMembers = true)]
internal class ClientInfo
Expand All @@ -24,6 +24,8 @@ internal class ClientInfo
[JsonProperty(ClientInfoClaim.UniqueTenantIdentifier)]
public string UniqueTenantIdentifier { get; set; }

public Dictionary<string, string> AdditionalResponseParameters { get; private set; }

public static ClientInfo CreateFromJson(string clientInfo)
{
if (string.IsNullOrEmpty(clientInfo))
Expand All @@ -35,7 +37,34 @@ public static ClientInfo CreateFromJson(string clientInfo)

try
{
return JsonHelper.DeserializeFromJson<ClientInfo>(Base64UrlHelpers.DecodeBytes(clientInfo));
var decodedBytes = Base64UrlHelpers.DecodeBytes(clientInfo);

// Deserialize into a dictionary to get all properties
var allProperties = JsonHelper.DeserializeFromJson<Dictionary<string, object>>(decodedBytes);

var clientInfoObj = new ClientInfo();
var additionalParams = new Dictionary<string, string>();

// Extract known claims and store the rest in AdditionalResponseParameters
foreach (var kvp in allProperties)
{
if (kvp.Key == ClientInfoClaim.UniqueIdentifier)
{
clientInfoObj.UniqueObjectIdentifier = kvp.Value?.ToString();

}
else if (kvp.Key == ClientInfoClaim.UniqueTenantIdentifier)
{
clientInfoObj.UniqueTenantIdentifier = kvp.Value?.ToString();
}
else
{
additionalParams[kvp.Key] = kvp.Value?.ToString() ?? string.Empty;
}
}

clientInfoObj.AdditionalResponseParameters = additionalParams;
return clientInfoObj;
}
catch (Exception exc)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,8 @@ private Dictionary<string, string> GetBodyParameters()
var dict = new Dictionary<string, string>
{
[OAuth2Parameter.GrantType] = OAuth2GrantType.ClientCredentials,
[OAuth2Parameter.Scope] = AuthenticationRequestParameters.Scope.AsSingleString()
[OAuth2Parameter.Scope] = AuthenticationRequestParameters.Scope.AsSingleString(),
[OAuth2Parameter.ClientInfo] = "2"
};

return dict;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -317,28 +317,28 @@ protected async Task<AuthenticationResult> CacheTokenResponseAndCreateAuthentica
// developer passed in user object.
AuthenticationRequestParameters.RequestContext.Logger.Info("Checking client info returned from the server..");

ClientInfo fromServer = null;
ClientInfo clientInfoFromServer = null;

if (!AuthenticationRequestParameters.IsClientCredentialRequest &&
AuthenticationRequestParameters.ApiId != ApiEvent.ApiIds.AcquireTokenForSystemAssignedManagedIdentity &&
if (AuthenticationRequestParameters.ApiId != ApiEvent.ApiIds.AcquireTokenForSystemAssignedManagedIdentity &&
AuthenticationRequestParameters.ApiId != ApiEvent.ApiIds.AcquireTokenForUserAssignedManagedIdentity &&
AuthenticationRequestParameters.ApiId != ApiEvent.ApiIds.AcquireTokenByRefreshToken &&
AuthenticationRequestParameters.AuthorityInfo.AuthorityType != AuthorityType.Adfs &&
!(msalTokenResponse.ClientInfo is null))
{
//client_info is not returned from client credential and managed identity flows because there is no user present.
fromServer = ClientInfo.CreateFromJson(msalTokenResponse.ClientInfo);
//client_info is not returned from managed identity flows because there is no user present.
clientInfoFromServer = ClientInfo.CreateFromJson(msalTokenResponse.ClientInfo);
ValidateAccountIdentifiers(clientInfoFromServer);
}

ValidateAccountIdentifiers(fromServer);

AuthenticationRequestParameters.RequestContext.Logger.Info("Saving token response to cache..");

var tuple = await CacheManager.SaveTokenResponseAsync(msalTokenResponse).ConfigureAwait(false);
var atItem = tuple.Item1;
var idtItem = tuple.Item2;
Account account = tuple.Item3;

atItem.AddPersistedCacheParameters(clientInfoFromServer.AdditionalResponseParameters);

return new AuthenticationResult(
atItem,
idtItem,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ async Task<Tuple<MsalAccessTokenCacheItem, MsalIdTokenCacheItem, Account>> IToke
CacheKeyFactory.GetOboKey(requestParams.LongRunningOboCacheKey, requestParams.UserAssertion),
requestParams.PersistedCacheParameters,
requestParams.CacheKeyComponents);
//TODO need client info here
}

if (!string.IsNullOrEmpty(response.RefreshToken))
Expand Down
23 changes: 22 additions & 1 deletion tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,18 @@ public static string GetMsiImdsErrorResponse()
"\"correlation_id\":\"77145480-bc5a-4ebe-ae4d-e4a8b7d727cf\",\"error_uri\":\"https://westus2.login.microsoft.com/error?code=500011\"}";
}

public static string CreateClientInfo(string uid = TestConstants.Uid, string utid = TestConstants.Utid)
public static string CreateClientInfo(string uid = TestConstants.Uid, string utid = TestConstants.Utid, bool addXms_acb = false)
{
if (addXms_acb)
{
if (!string.IsNullOrEmpty(uid))
{
return Base64UrlHelpers.Encode("{\"uid\":\"" + uid + "\",\"utid\":\"" + utid + "\",\"xms_acb\":[\"value1\",\"value2\"]}");
}

return Base64UrlHelpers.Encode("{\"xms_acb\":[\"value1\",\"value2\"]}");
}

return Base64UrlHelpers.Encode("{\"uid\":\"" + uid + "\",\"utid\":\"" + utid + "\"}");
}

Expand Down Expand Up @@ -354,6 +364,17 @@ public static HttpResponseMessage CreateSuccessfulClientCredentialTokenResponseM
"{\"token_type\":\"" + tokenType + "\",\"expires_in\":\"" + expiry + "\",\"access_token\":\"" + token + "\",\"additional_param1\":\"value1\",\"additional_param2\":\"value2\",\"additional_param3\":\"value3\"}");
}

public static HttpResponseMessage CreateSuccessfulClientCredentialTokenResponseWithClientInfoMessage(
string token = "header.payload.signature",
string expiry = "3599",
string tokenType = "Bearer",
bool addXms_acb = false
)
{
return CreateSuccessResponseMessage(
"{\"token_type\":\"" + tokenType + "\",\"expires_in\":\"" + expiry + "\",\"access_token\":\"" + token + "\",\"additional_param1\":\"value1\",\"additional_param2\":\"value2\",\"additional_param3\":\"value3\",\"client_info\":\"" + CreateClientInfo(null, null, addXms_acb) + "\"}");
}

public static HttpResponseMessage CreateSuccessfulClientCredentialTokenResponseWithAdditionalParamsMessage(
string token = "header.payload.signature",
string expiry = "3599",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,18 +183,20 @@ public static void AddMockHandlerContentNotFound(this MockHttpManager httpManage
}

public static MockHttpMessageHandler AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(
this MockHttpManager httpManager,
string token = "header.payload.signature",
this MockHttpManager httpManager,
string token = "header.payload.signature",
string expiresIn = "3599",
string tokenType = "Bearer",
IList<string> unexpectedHttpHeaders = null,
Dictionary<string, string> expectedPostData = null
Dictionary<string, string> expectedPostData = null,
bool addClientInfo = false
)
{
var handler = new MockHttpMessageHandler()
{
ExpectedMethod = HttpMethod.Post,
ResponseMessage = MockHelpers.CreateSuccessfulClientCredentialTokenResponseMessage(token, expiresIn, tokenType),
ResponseMessage = addClientInfo? MockHelpers.CreateSuccessfulClientCredentialTokenResponseWithClientInfoMessage(token, expiresIn, tokenType, addClientInfo)
: MockHelpers.CreateSuccessfulClientCredentialTokenResponseMessage(token, expiresIn, tokenType),
UnexpectedRequestHeaders = unexpectedHttpHeaders,
ExpectedPostData = expectedPostData
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
using static Microsoft.Identity.Client.Internal.JsonWebToken;
using Microsoft.Identity.Client.RP;
using Microsoft.Identity.Client.Http;
using Microsoft.Identity.Client.OAuth2;

namespace Microsoft.Identity.Test.Unit
{
Expand Down Expand Up @@ -1019,6 +1020,43 @@ public void EnsureNullCertDoesNotSetSerialNumberTestAsync()
}
}

[TestMethod]
public async Task AcquireTokenForClient_ShouldSendClientInfoParameter_WithValueTwo_Async()
{
// Arrange
using (var httpManager = new MockHttpManager())
{
httpManager.AddInstanceDiscoveryMockHandler();

// Set up the expected POST data to include client_info = "2"
var expectedPostData = new Dictionary<string, string>
{
[OAuth2Parameter.GrantType] = OAuth2GrantType.ClientCredentials,
[OAuth2Parameter.Scope] = TestConstants.s_scope.AsSingleString(),
[OAuth2Parameter.ClientInfo] = "2"
};

var handler = httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(
expectedPostData: expectedPostData);

var app = ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
.WithClientSecret(TestConstants.ClientSecret)
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithHttpManager(httpManager)
.BuildConcrete();

// Act
var result = await app.AcquireTokenForClient(TestConstants.s_scope)
.ExecuteAsync()
.ConfigureAwait(false);

// Assert
Assert.IsNotNull(result);
Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource);
}
}

private void BeforeCacheAccess(TokenCacheNotificationArgs args)
{
args.TokenCache.DeserializeMsalV3(_serializedCache);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2320,6 +2320,63 @@ public async Task AcquireTokenForClient_WithClaims_And_MismatchedHash_UsesCache_
}
}

[TestMethod]
public async Task ConfidentialClient_acquireTokenForClient_ReturnsXmsAcbTestAsync()
{
using (var httpManager = new MockHttpManager())
{
httpManager.AddInstanceDiscoveryMockHandler();

var app = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId)
.WithAuthority(new Uri(ClientApplicationBase.DefaultAuthority), true)
.WithRedirectUri(TestConstants.RedirectUri)
.WithClientSecret(TestConstants.ClientSecret)
.WithHttpManager(httpManager)
.BuildConcrete();

httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(addClientInfo: true);
var appCacheAccess = app.AppTokenCache.RecordAccess();
var userCacheAccess = app.UserTokenCache.RecordAccess();

var result = await app.AcquireTokenForClient(TestConstants.s_scope.ToArray()).ExecuteAsync(CancellationToken.None).ConfigureAwait(false);
Assert.IsNotNull(result);
Assert.AreEqual("header.payload.signature", result.AccessToken);
Assert.AreEqual(TestConstants.s_scope.AsSingleString(), result.Scopes.AsSingleString());
Assert.IsTrue(result.AdditionalResponseParameters.ContainsKey("xms_acb"));
Assert.AreEqual("value1 value2", result.AdditionalResponseParameters["xms_acb"]);

// make sure user token cache is empty
Assert.AreEqual(0, app.UserTokenCacheInternal.Accessor.GetAllAccessTokens().Count);
Assert.AreEqual(0, app.UserTokenCacheInternal.Accessor.GetAllRefreshTokens().Count);

// check app token cache count to be 1
Assert.AreEqual(1, app.AppTokenCacheInternal.Accessor.GetAllAccessTokens().Count);
Assert.AreEqual(0, app.AppTokenCacheInternal.Accessor.GetAllRefreshTokens().Count);

appCacheAccess.AssertAccessCounts(1, 1);
userCacheAccess.AssertAccessCounts(0, 0);

// call AcquireTokenForClientAsync again to get result back from the cache
result = await app.AcquireTokenForClient(TestConstants.s_scope.ToArray()).ExecuteAsync(CancellationToken.None).ConfigureAwait(false);
Assert.IsNotNull(result);
Assert.AreEqual("header.payload.signature", result.AccessToken);
Assert.AreEqual(TestConstants.s_scope.AsSingleString(), result.Scopes.AsSingleString());
Assert.IsTrue(result.AdditionalResponseParameters.ContainsKey("xms_acb"));
Assert.AreEqual("value1 value2", result.AdditionalResponseParameters["xms_acb"]);

// make sure user token cache is empty
Assert.AreEqual(0, app.UserTokenCacheInternal.Accessor.GetAllAccessTokens().Count);
Assert.AreEqual(0, app.UserTokenCacheInternal.Accessor.GetAllRefreshTokens().Count);

// check app token cache count to be 1
Assert.AreEqual(1, app.AppTokenCacheInternal.Accessor.GetAllAccessTokens().Count);
Assert.AreEqual(0, app.AppTokenCacheInternal.Accessor.GetAllRefreshTokens().Count);

appCacheAccess.AssertAccessCounts(2, 1);
userCacheAccess.AssertAccessCounts(0, 0);
}
}

private static string ComputeSHA256Hex(string token)
{
var cryptoMgr = new CommonCryptographyManager();
Expand Down
Loading