Skip to content
38 changes: 34 additions & 4 deletions src/client/Microsoft.Identity.Client/AuthenticationResult.cs
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 @@ -135,7 +136,8 @@ internal AuthenticationResult(
ApiEvent apiEvent,
Account account,
string spaAuthCode,
IReadOnlyDictionary<string, string> additionalResponseParameters)
IReadOnlyDictionary<string, string> additionalResponseParameters,
string acbAuthN = null)
{
_authenticationScheme = authenticationScheme ?? throw new ArgumentNullException(nameof(authenticationScheme));

Expand Down Expand Up @@ -167,9 +169,37 @@ internal AuthenticationResult(
CorrelationId = correlationID;
ApiEvent = apiEvent;
AuthenticationResultMetadata = new AuthenticationResultMetadata(tokenSource);
AdditionalResponseParameters = msalAccessTokenCacheItem?.PersistedCacheParameters?.Count > 0 ?
(IReadOnlyDictionary<string, string>)msalAccessTokenCacheItem.PersistedCacheParameters :
additionalResponseParameters;

if (!string.IsNullOrEmpty(acbAuthN))
{
if (msalAccessTokenCacheItem.PersistedCacheParameters?.Count > 0)
{
msalAccessTokenCacheItem.PersistedCacheParameters.Add("xms_acb", acbAuthN);
AdditionalResponseParameters = (IReadOnlyDictionary<string, string>)msalAccessTokenCacheItem.PersistedCacheParameters;
}
else
{
if (additionalResponseParameters == null)
{
additionalResponseParameters = new Dictionary<string, string> { { "xms_acb", acbAuthN } };
}
else
{
Dictionary<string, string> tempParams = additionalResponseParameters.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
tempParams.Add("xms_acb", acbAuthN);
additionalResponseParameters = tempParams;
}

AdditionalResponseParameters = additionalResponseParameters;
}
}
else
{
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 @@ -37,7 +37,8 @@ internal MsalAccessTokenCacheItem(
string keyId = null,
string oboCacheKey = null,
IEnumerable<string> persistedCacheParameters = null,
SortedList<string, string> cacheKeyComponents = null)
SortedList<string, string> cacheKeyComponents = null,
string acbAuthN = null)
: this(
scopes: ScopeHelper.OrderScopesAlphabetically(response.Scope), // order scopes to avoid cache duplication. This is not in the hot path.
cachedAt: DateTimeOffset.UtcNow,
Expand All @@ -54,6 +55,7 @@ internal MsalAccessTokenCacheItem(
RawClientInfo = response.ClientInfo;
HomeAccountId = homeAccountId;
OboCacheKey = oboCacheKey;
AcbAuthN = acbAuthN;

InitializeAdditionalCacheKeyComponents(cacheKeyComponents);
#if !MOBILE
Expand Down Expand Up @@ -288,6 +290,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 Expand Up @@ -322,6 +326,7 @@ internal static MsalAccessTokenCacheItem FromJObject(JObject j)
string scopes = JsonHelper.ExtractExistingOrEmptyString(j, StorageJsonKeys.Target);
var additionalCacheKeyComponents = JsonHelper.ExtractInnerJsonAsDictionary(j, StorageJsonKeys.CacheExtensions);
var persistedCacheParameters = JsonHelper.ExtractInnerJsonAsDictionary(j, StorageJsonKeys.PersistedCacheParameters);
string acbAuthN = JsonHelper.ExtractExistingOrDefault<string>(j, StorageJsonKeys.AcbAuthN);

var item = new MsalAccessTokenCacheItem(
scopes: scopes,
Expand All @@ -341,6 +346,7 @@ internal static MsalAccessTokenCacheItem FromJObject(JObject j)

item.PersistedCacheParameters = persistedCacheParameters;
item.OboCacheKey = oboCacheKey;
item.AcbAuthN = acbAuthN;
item.PopulateFieldsFromJObject(j);

item.InitCacheKey();
Expand All @@ -365,6 +371,7 @@ internal override JObject ToJObject()
json,
StorageJsonKeys.RefreshOn,
RefreshOn.HasValue ? DateTimeHelpers.DateTimeToUnixTimestamp(RefreshOn.Value) : null);
SetItemIfValueNotNull(json, StorageJsonKeys.AcbAuthN, AcbAuthN);

// previous versions of MSAL used "ext_expires_on" instead of the correct "extended_expires_on".
// this is here for back compatibility
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,6 @@ internal static class StorageJsonKeys

public const string CacheExtensions = "ext";
public const string PersistedCacheParameters = "persisted_cache_parameters";
public const string AcbAuthN = "xms_acb";
}
}
6 changes: 6 additions & 0 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 @@ -24,6 +25,11 @@ internal class ClientInfo
[JsonProperty(ClientInfoClaim.UniqueTenantIdentifier)]
public string UniqueTenantIdentifier { get; set; }

[JsonProperty(ClientInfoClaim.AcbAuthN)]
private IEnumerable<string> AcbAuthNRaw { get; set; }

public string AcbAuthN => string.Join(" ", AcbAuthNRaw);

public static ClientInfo CreateFromJson(string clientInfo)
{
if (string.IsNullOrEmpty(clientInfo))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ internal static class ClientInfoClaim
{
public const string UniqueIdentifier = "uid";
public const string UniqueTenantIdentifier = "utid";
public const string AcbAuthN = "xms_acb";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,8 @@ private AuthenticationResult CreateAuthenticationResultFromCache(MsalAccessToken
AuthenticationRequestParameters.RequestContext.ApiEvent,
account: null,
spaAuthCode: null,
additionalResponseParameters: null);
additionalResponseParameters: null,
acbAuthN: cachedAccessTokenItem.AcbAuthN);
return authResult;
}

Expand All @@ -335,7 +336,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,29 @@ 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);

msalTokenResponse.AcbAuthN = clientInfoFromServer.AcbAuthN;
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;

//TODO Get client info from response and use it to

return new AuthenticationResult(
atItem,
idtItem,
Expand All @@ -348,7 +349,8 @@ protected async Task<AuthenticationResult> CacheTokenResponseAndCreateAuthentica
AuthenticationRequestParameters.RequestContext.ApiEvent,
account,
msalTokenResponse.SpaAuthCode,
msalTokenResponse.CreateExtensionDataStringMap());
msalTokenResponse.CreateExtensionDataStringMap(),
acbAuthN: clientInfoFromServer.AcbAuthN);
}

protected virtual void ValidateAccountIdentifiers(ClientInfo fromServer)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ public IReadOnlyDictionary<string, string> CreateExtensionDataStringMap()

public HttpResponse HttpResponse { get; set; }

public string AcbAuthN { get; set; }

internal static MsalTokenResponse CreateFromiOSBrokerResponse(Dictionary<string, string> responseDictionary)
{
if (responseDictionary.TryGetValue(BrokerResponseConst.BrokerErrorCode, out string errorCode))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ async Task<Tuple<MsalAccessTokenCacheItem, MsalIdTokenCacheItem, Account>> IToke
requestParams.AuthenticationScheme.KeyId,
CacheKeyFactory.GetOboKey(requestParams.LongRunningOboCacheKey, requestParams.UserAssertion),
requestParams.PersistedCacheParameters,
requestParams.CacheKeyComponents);
requestParams.CacheKeyComponents,
response.AcbAuthN);
//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
Loading
Loading