Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,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 AddAdditionalCacheParameters(Dictionary<string, string> additionalCacheParameters)
{
if (additionalCacheParameters != null)
{
if (PersistedCacheParameters == null)
{
PersistedCacheParameters = new Dictionary<string, string>(additionalCacheParameters);
}
else
{
foreach (var kvp in additionalCacheParameters)
{
PersistedCacheParameters[kvp.Key] = kvp.Value;
}
}
}
}

#endif
internal /* for test */ MsalAccessTokenCacheItem(
string preferredCacheEnv,
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.AddAdditionalCacheParameters(clientInfoFromServer?.AdditionalResponseParameters);

return new AuthenticationResult(
atItem,
idtItem,
Expand Down
18 changes: 17 additions & 1 deletion tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,13 @@ 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 CreateClientInfoForS2S = false)
{
if (CreateClientInfoForS2S)
{
return Base64UrlHelpers.Encode("{\"authZ\":[\"value1\",\"value2\"]}");
}

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

Expand Down Expand Up @@ -368,6 +373,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 CreateClientInfoForS2S = 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, CreateClientInfoForS2S) + "\"}");
}

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, true)
: 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 @@ -2288,6 +2288,63 @@ public async Task AcquireTokenForClient_WithClaims_And_MismatchedHash_UsesCache_
}
}

[TestMethod]
public async Task ConfidentialClient_acquireTokenForClient_ReturnsAuthZTestAsync()
{
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("authZ"));
Assert.AreEqual("[\r\n \"value1\",\r\n \"value2\"\r\n]", result.AdditionalResponseParameters["authZ"]);

// 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("authZ"));
Assert.AreEqual("[\r\n \"value1\",\r\n \"value2\"\r\n]", result.AdditionalResponseParameters["authZ"]);

// 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