Skip to content
Merged
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 @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System.Collections.Generic;
using System.Threading;

namespace Microsoft.Identity.Client.AuthScheme
{
Expand All @@ -14,5 +15,10 @@ public class MsalCacheValidationData
/// Gets the persisted parameters addded to the cache items.
/// </summary>
public IDictionary<string, string> PersistedCacheParameters { get; internal set; }

/// <summary>
/// The cancellation token used to cancel cache validation operations.
/// </summary>
public CancellationToken cancellationToken { get; internal set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Client.ApiConfig.Parameters;
using Microsoft.Identity.Client.AuthScheme;
using Microsoft.Identity.Client.Cache.Items;
using Microsoft.Identity.Client.Core;
using Microsoft.Identity.Client.Extensibility;
Expand Down Expand Up @@ -72,20 +71,8 @@ protected override async Task<AuthenticationResult> ExecuteAsync(CancellationTok

MsalAccessTokenCacheItem cachedAccessTokenItem = await GetCachedAccessTokenAsync().ConfigureAwait(false);

// Validate the cached token using the authentication operation
if (AuthenticationRequestParameters.AuthenticationScheme != null &&
cachedAccessTokenItem != null &&
AuthenticationRequestParameters.AuthenticationScheme is IAuthenticationOperation2 authOp2)
{
var cacheValidationData = new MsalCacheValidationData();
cacheValidationData.PersistedCacheParameters = cachedAccessTokenItem.PersistedCacheParameters;

if (!await authOp2.ValidateCachedTokenAsync(cacheValidationData).ConfigureAwait(false))
{
logger.Info("[ClientCredentialRequest] Cached token failed authentication operation validation.");
cachedAccessTokenItem = null;
}
}
cachedAccessTokenItem = await ValidateCachedAccessTokenAsync(
AuthenticationRequestParameters, cachedAccessTokenItem, nameof(ClientCredentialRequest)).ConfigureAwait(false);

// No access token or cached access token needs to be refreshed
if (cachedAccessTokenItem != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ protected override async Task<AuthenticationResult> ExecuteAsync(CancellationTok
cachedAccessToken = await CacheManager.FindAccessTokenAsync().ConfigureAwait(false);
}

cachedAccessToken = await ValidateCachedAccessTokenAsync(
AuthenticationRequestParameters, cachedAccessToken, nameof(OnBehalfOfRequest)).ConfigureAwait(false);

if (cachedAccessToken != null)
{
var cachedIdToken = await CacheManager.GetIdTokenCacheItemAsync(cachedAccessToken).ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -570,5 +570,31 @@ private static RegionDetails CreateRegionDetails(ApiEvent apiEvent)
apiEvent.RegionUsed,
apiEvent.RegionDiscoveryFailureReason);
}

/// <summary>
/// Validates a cached access token using the authentication operation, if the scheme implements <see cref="IAuthenticationOperation2"/>.
/// Returns the original cache item if validation passes or is not applicable, or null if validation fails.
/// </summary>
internal static async Task<MsalAccessTokenCacheItem> ValidateCachedAccessTokenAsync(
AuthenticationRequestParameters authenticationRequestParameters,
MsalAccessTokenCacheItem cachedAccessTokenItem,
string requestType)
{
if (cachedAccessTokenItem != null &&
authenticationRequestParameters.AuthenticationScheme is IAuthenticationOperation2 authOp2)
{
var cacheValidationData = new MsalCacheValidationData();
cacheValidationData.PersistedCacheParameters = cachedAccessTokenItem.PersistedCacheParameters;

if (!await authOp2.ValidateCachedTokenAsync(cacheValidationData).ConfigureAwait(false))
{
authenticationRequestParameters.RequestContext.Logger.Info(
$"[{requestType}] Cached token failed authentication operation validation.");
return null;
}
}

return cachedAccessTokenItem;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ public async Task<AuthenticationResult> ExecuteAsync(CancellationToken cancellat
{
cachedAccessTokenItem = await CacheManager.FindAccessTokenAsync().ConfigureAwait(false);

cachedAccessTokenItem = await RequestBase.ValidateCachedAccessTokenAsync(
AuthenticationRequestParameters, cachedAccessTokenItem, nameof(CacheSilentStrategy)).ConfigureAwait(false);

if (cachedAccessTokenItem != null)
{
logger.Info("Returning access token found in cache. RefreshOn exists ? "
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.cancellationToken.get -> System.Threading.CancellationToken
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.cancellationToken.get -> System.Threading.CancellationToken
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.cancellationToken.get -> System.Threading.CancellationToken
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.cancellationToken.get -> System.Threading.CancellationToken
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.cancellationToken.get -> System.Threading.CancellationToken
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.cancellationToken.get -> System.Threading.CancellationToken
Original file line number Diff line number Diff line change
Expand Up @@ -460,10 +460,210 @@ public async Task ValidateCachedTokenAsync_WithNullCachedItem_ValidationNotCalle
// Assert - validation was never called because no cached token existed
await authScheme.DidNotReceive().ValidateCachedTokenAsync(Arg.Any<MsalCacheValidationData>())
.ConfigureAwait(false);

Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource);
Assert.AreEqual(0, httpManager.QueueSize);
}
}

[TestMethod]
public async Task ValidateCachedTokenAsync_OBO_WhenValidationFails_CacheIsIgnoredAsync()
{
// Arrange
var authScheme = Substitute.For<IAuthenticationOperation2>();
authScheme.AuthorizationHeaderPrefix.Returns("CustomToken");
authScheme.AccessTokenType.Returns("bearer");
authScheme.KeyId.Returns("keyid");
authScheme.GetTokenRequestParams().Returns(new Dictionary<string, string>() { { "tokenParam", "tokenParamValue" } });

authScheme.WhenForAnyArgs(x => x.FormatResultAsync(default, default))
.Do(x => ((AuthenticationResult)x[0]).AccessToken = "validated_" + ((AuthenticationResult)x[0]).AccessToken);

// Validation fails - cached token should be ignored
authScheme.ValidateCachedTokenAsync(Arg.Any<MsalCacheValidationData>())
.Returns(Task.FromResult(false));

using (var httpManager = new MockHttpManager())
{
httpManager.AddInstanceDiscoveryMockHandler();

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

// First request - acquire initial token via OBO
httpManager.AddSuccessTokenResponseMockHandlerForPost(TestConstants.AuthorityCommonTenant);

UserAssertion userAssertion = new UserAssertion(TestConstants.DefaultAccessToken);
var result1 = await cca.AcquireTokenOnBehalfOf(TestConstants.s_scope, userAssertion)
.WithAuthenticationOperation(authScheme)
.ExecuteAsync()
.ConfigureAwait(false);

Assert.IsNotNull(result1);
Assert.AreEqual(TokenSource.IdentityProvider, result1.AuthenticationResultMetadata.TokenSource);

// Second request - validation fails, so a new token should be acquired
httpManager.AddSuccessTokenResponseMockHandlerForPost(TestConstants.AuthorityCommonTenant);

var result2 = await cca.AcquireTokenOnBehalfOf(TestConstants.s_scope, userAssertion)
.WithAuthenticationOperation(authScheme)
.ExecuteAsync()
.ConfigureAwait(false);

Assert.AreEqual(TokenSource.IdentityProvider, result2.AuthenticationResultMetadata.TokenSource);
await authScheme.Received(1).ValidateCachedTokenAsync(Arg.Any<MsalCacheValidationData>())
.ConfigureAwait(false);
Assert.AreEqual(0, httpManager.QueueSize);
}
}

[TestMethod]
public async Task ValidateCachedTokenAsync_OBO_WhenValidationSucceeds_CacheIsUsedAsync()
{
// Arrange
var authScheme = Substitute.For<IAuthenticationOperation2>();
authScheme.AuthorizationHeaderPrefix.Returns("CustomToken");
authScheme.AccessTokenType.Returns("bearer");
authScheme.KeyId.Returns("keyid");
authScheme.GetTokenRequestParams().Returns(new Dictionary<string, string>() { { "tokenParam", "tokenParamValue" } });

authScheme.WhenForAnyArgs(x => x.FormatResultAsync(default, default))
.Do(x => ((AuthenticationResult)x[0]).AccessToken = "validated_" + ((AuthenticationResult)x[0]).AccessToken);

// Validation succeeds - cached token should be used
authScheme.ValidateCachedTokenAsync(Arg.Any<MsalCacheValidationData>())
.Returns(Task.FromResult(true));

using (var httpManager = new MockHttpManager())
{
httpManager.AddInstanceDiscoveryMockHandler();

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

// First request - acquire initial token via OBO
httpManager.AddSuccessTokenResponseMockHandlerForPost(TestConstants.AuthorityCommonTenant);

UserAssertion userAssertion = new UserAssertion(TestConstants.DefaultAccessToken);
var result1 = await cca.AcquireTokenOnBehalfOf(TestConstants.s_scope, userAssertion)
.WithAuthenticationOperation(authScheme)
.ExecuteAsync()
.ConfigureAwait(false);

Assert.IsNotNull(result1);
Assert.AreEqual(TokenSource.IdentityProvider, result1.AuthenticationResultMetadata.TokenSource);

// Second request - validation succeeds, cached token should be returned
var result2 = await cca.AcquireTokenOnBehalfOf(TestConstants.s_scope, userAssertion)
.WithAuthenticationOperation(authScheme)
.ExecuteAsync()
.ConfigureAwait(false);

Assert.AreEqual(TokenSource.Cache, result2.AuthenticationResultMetadata.TokenSource);
await authScheme.Received(1).ValidateCachedTokenAsync(Arg.Any<MsalCacheValidationData>())
.ConfigureAwait(false);
Assert.AreEqual(0, httpManager.QueueSize);
}
}

[TestMethod]
public async Task ValidateCachedTokenAsync_Silent_WhenValidationFails_CacheIsIgnoredAsync()
{
// Arrange
var authScheme = Substitute.For<IAuthenticationOperation2>();
authScheme.AuthorizationHeaderPrefix.Returns("CustomToken");
authScheme.AccessTokenType.Returns("bearer");
authScheme.GetTokenRequestParams().Returns(new Dictionary<string, string>() { { "tokenParam", "tokenParamValue" } });

authScheme.WhenForAnyArgs(x => x.FormatResultAsync(default, default))
.Do(x => ((AuthenticationResult)x[0]).AccessToken = "validated_" + ((AuthenticationResult)x[0]).AccessToken);

// Validation fails - cached token should be ignored
authScheme.ValidateCachedTokenAsync(Arg.Any<MsalCacheValidationData>())
.Returns(Task.FromResult(false));

using (var httpManager = new MockHttpManager())
{
httpManager.AddInstanceDiscoveryMockHandler();

var cca = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId)
.WithExperimentalFeatures()
.WithClientSecret(TestConstants.ClientSecret)
.WithAuthority(new Uri(TestConstants.AuthorityTestTenant), true)
.WithHttpManager(httpManager)
.BuildConcrete();

// Populate user token cache with a valid access token and refresh token
TokenCacheHelper.PopulateCache(cca.UserTokenCacheInternal.Accessor, addSecondAt: false);

// Silent request - validation should fail, so RT refresh should occur
httpManager.AddSuccessTokenResponseMockHandlerForPost(TestConstants.AuthorityTestTenant);

var account = (await cca.GetAccountsAsync().ConfigureAwait(false)).Single();

var result = await cca.AcquireTokenSilent(TestConstants.s_scope, account)
.WithAuthenticationOperation(authScheme)
.ExecuteAsync()
.ConfigureAwait(false);

// Assert - validation was called and failed, token was refreshed via RT
Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource);
await authScheme.Received(1).ValidateCachedTokenAsync(Arg.Any<MsalCacheValidationData>())
.ConfigureAwait(false);
Assert.AreEqual(0, httpManager.QueueSize);
}
}

[TestMethod]
public async Task ValidateCachedTokenAsync_Silent_WhenValidationSucceeds_CacheIsUsedAsync()
{
// Arrange
var authScheme = Substitute.For<IAuthenticationOperation2>();
authScheme.AuthorizationHeaderPrefix.Returns("CustomToken");
authScheme.AccessTokenType.Returns("bearer");
authScheme.GetTokenRequestParams().Returns(new Dictionary<string, string>() { { "tokenParam", "tokenParamValue" } });

authScheme.WhenForAnyArgs(x => x.FormatResultAsync(default, default))
.Do(x => ((AuthenticationResult)x[0]).AccessToken = "validated_" + ((AuthenticationResult)x[0]).AccessToken);

// Validation succeeds - cached token should be used
authScheme.ValidateCachedTokenAsync(Arg.Any<MsalCacheValidationData>())
.Returns(Task.FromResult(true));

using (var httpManager = new MockHttpManager())
{
var cca = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId)
.WithExperimentalFeatures()
.WithClientSecret(TestConstants.ClientSecret)
.WithAuthority(new Uri(TestConstants.AuthorityTestTenant), true)
.WithHttpManager(httpManager)
.BuildConcrete();

// Populate user token cache with a valid access token and refresh token
TokenCacheHelper.PopulateCache(cca.UserTokenCacheInternal.Accessor, addSecondAt: false);

var account = (await cca.GetAccountsAsync().ConfigureAwait(false)).Single();

// Silent request - validation succeeds, cached token should be returned
var result = await cca.AcquireTokenSilent(TestConstants.s_scope, account)
.WithAuthenticationOperation(authScheme)
.ExecuteAsync()
.ConfigureAwait(false);

// Assert - validation was called and succeeded, cached token was returned
Assert.AreEqual(TokenSource.Cache, result.AuthenticationResultMetadata.TokenSource);
await authScheme.Received(1).ValidateCachedTokenAsync(Arg.Any<MsalCacheValidationData>())
.ConfigureAwait(false);
Assert.AreEqual(0, httpManager.QueueSize);
}
}
}
}