diff --git a/src/client/Microsoft.Identity.Client/AuthScheme/Bearer/BearerAuthenticationOperation.cs b/src/client/Microsoft.Identity.Client/AuthScheme/Bearer/BearerAuthenticationOperation.cs index b512fc78a7..e8679a6400 100644 --- a/src/client/Microsoft.Identity.Client/AuthScheme/Bearer/BearerAuthenticationOperation.cs +++ b/src/client/Microsoft.Identity.Client/AuthScheme/Bearer/BearerAuthenticationOperation.cs @@ -38,5 +38,11 @@ public IReadOnlyDictionary GetTokenRequestParams() // ESTS issues Bearer tokens by default, no need for any extra params return CollectionHelpers.GetEmptyDictionary(); } + + public Task ValidateCachedTokenAsync(MsalCacheValidationData cachedTokenData) + { + // no-op + return Task.FromResult(true); + } } } diff --git a/src/client/Microsoft.Identity.Client/AuthScheme/IAuthenticationOperation2.cs b/src/client/Microsoft.Identity.Client/AuthScheme/IAuthenticationOperation2.cs index 1bb1eaf932..f43c6ec052 100644 --- a/src/client/Microsoft.Identity.Client/AuthScheme/IAuthenticationOperation2.cs +++ b/src/client/Microsoft.Identity.Client/AuthScheme/IAuthenticationOperation2.cs @@ -17,5 +17,12 @@ public interface IAuthenticationOperation2 : IAuthenticationOperation /// Will be invoked instead of IAuthenticationOperation.FormatResult /// Task FormatResultAsync(AuthenticationResult authenticationResult, CancellationToken cancellationToken = default); + + /// + /// Determines whether the cached token is still valid. + /// + /// Data used to determine if token is still valid + /// + Task ValidateCachedTokenAsync(MsalCacheValidationData cachedTokenData); } } diff --git a/src/client/Microsoft.Identity.Client/AuthScheme/MsalCacheValidationData.cs b/src/client/Microsoft.Identity.Client/AuthScheme/MsalCacheValidationData.cs new file mode 100644 index 0000000000..a0e95d28c8 --- /dev/null +++ b/src/client/Microsoft.Identity.Client/AuthScheme/MsalCacheValidationData.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace Microsoft.Identity.Client.AuthScheme +{ + /// + /// Data used to validate cache items for different authentication schemes. + /// + public class MsalCacheValidationData + { + /// + /// Gets the persisted parameters addded to the cache items. + /// + public IDictionary PersistedCacheParameters { get; internal set; } + } +} diff --git a/src/client/Microsoft.Identity.Client/AuthScheme/PoP/MtlsPopAuthenticationOperation.cs b/src/client/Microsoft.Identity.Client/AuthScheme/PoP/MtlsPopAuthenticationOperation.cs index f74f1b0859..4bd1913331 100644 --- a/src/client/Microsoft.Identity.Client/AuthScheme/PoP/MtlsPopAuthenticationOperation.cs +++ b/src/client/Microsoft.Identity.Client/AuthScheme/PoP/MtlsPopAuthenticationOperation.cs @@ -48,5 +48,11 @@ public void FormatResult(AuthenticationResult authenticationResult) { authenticationResult.BindingCertificate = _mtlsCert; } + + public Task ValidateCachedTokenAsync(MsalCacheValidationData cachedTokenData) + { + // no-op + return Task.FromResult(true); + } } } diff --git a/src/client/Microsoft.Identity.Client/AuthScheme/PoP/PopAuthenticationOperation.cs b/src/client/Microsoft.Identity.Client/AuthScheme/PoP/PopAuthenticationOperation.cs index 934d42d688..44b5856b89 100644 --- a/src/client/Microsoft.Identity.Client/AuthScheme/PoP/PopAuthenticationOperation.cs +++ b/src/client/Microsoft.Identity.Client/AuthScheme/PoP/PopAuthenticationOperation.cs @@ -172,5 +172,11 @@ private string CreateJWS(string payload, string header) return sb.ToString(); } + + public Task ValidateCachedTokenAsync(MsalCacheValidationData cachedTokenData) + { + // no-op + return Task.FromResult(true); + } } } diff --git a/src/client/Microsoft.Identity.Client/AuthScheme/PoP/PopBrokerAuthenticationOperation.cs b/src/client/Microsoft.Identity.Client/AuthScheme/PoP/PopBrokerAuthenticationOperation.cs index e9e6e7a388..fc27d10276 100644 --- a/src/client/Microsoft.Identity.Client/AuthScheme/PoP/PopBrokerAuthenticationOperation.cs +++ b/src/client/Microsoft.Identity.Client/AuthScheme/PoP/PopBrokerAuthenticationOperation.cs @@ -40,5 +40,11 @@ public IReadOnlyDictionary GetTokenRequestParams() { return CollectionHelpers.GetEmptyDictionary(); } + + public Task ValidateCachedTokenAsync(MsalCacheValidationData cachedTokenData) + { + // no-op + return Task.FromResult(true); + } } } diff --git a/src/client/Microsoft.Identity.Client/AuthScheme/SSHCertificates/SSHCertAuthenticationOperation.cs b/src/client/Microsoft.Identity.Client/AuthScheme/SSHCertificates/SSHCertAuthenticationOperation.cs index 5a5a7cd04e..caccfbe2e4 100644 --- a/src/client/Microsoft.Identity.Client/AuthScheme/SSHCertificates/SSHCertAuthenticationOperation.cs +++ b/src/client/Microsoft.Identity.Client/AuthScheme/SSHCertificates/SSHCertAuthenticationOperation.cs @@ -60,5 +60,11 @@ public IReadOnlyDictionary GetTokenRequestParams() { OAuth2Parameter.RequestConfirmation , _jwk } }; } + + public Task ValidateCachedTokenAsync(MsalCacheValidationData cachedTokenData) + { + // no-op + return Task.FromResult(true); + } } } diff --git a/src/client/Microsoft.Identity.Client/Extensibility/ExternalBoundTokenScheme.cs b/src/client/Microsoft.Identity.Client/Extensibility/ExternalBoundTokenScheme.cs index 84e1830893..7518d6c8c2 100644 --- a/src/client/Microsoft.Identity.Client/Extensibility/ExternalBoundTokenScheme.cs +++ b/src/client/Microsoft.Identity.Client/Extensibility/ExternalBoundTokenScheme.cs @@ -44,5 +44,11 @@ public IReadOnlyDictionary GetTokenRequestParams() { return CollectionHelpers.GetEmptyDictionary(); } + + public Task ValidateCachedTokenAsync(MsalCacheValidationData cachedTokenData) + { + // no-op + return Task.FromResult(true); + } } } diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/ClientCredentialRequest.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/ClientCredentialRequest.cs index 5e120eec6a..ad61253b96 100644 --- a/src/client/Microsoft.Identity.Client/Internal/Requests/ClientCredentialRequest.cs +++ b/src/client/Microsoft.Identity.Client/Internal/Requests/ClientCredentialRequest.cs @@ -10,6 +10,7 @@ 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; @@ -76,6 +77,21 @@ protected override async Task 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; + } + } + // No access token or cached access token needs to be refreshed if (cachedAccessTokenItem != null) { diff --git a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj index 3279f0338a..5c7f577e0e 100644 --- a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj +++ b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj @@ -92,7 +92,7 @@ - + @@ -104,7 +104,7 @@ - + @@ -161,4 +161,4 @@ - + \ No newline at end of file diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt index c000207bbf..058ab31a1f 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt @@ -2,6 +2,10 @@ const Microsoft.Identity.Client.MsalError.CannotSwitchBetweenImdsVersionsForPrev const Microsoft.Identity.Client.MsalError.InvalidCertificate = "invalid_certificate" -> string const Microsoft.Identity.Client.MsalError.MtlsNotSupportedForManagedIdentity = "mtls_not_supported_for_managed_identity" -> string const Microsoft.Identity.Client.MsalError.MtlsPopTokenNotSupportedinImdsV1 = "mtls_pop_token_not_supported_in_imds_v1" -> string +Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2.ValidateCachedTokenAsync(Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData cachedTokenData) -> System.Threading.Tasks.Task +Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData +Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.MsalCacheValidationData() -> void +Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.PersistedCacheParameters.get -> System.Collections.Generic.IDictionary Microsoft.Identity.Client.IMsalMtlsHttpClientFactory Microsoft.Identity.Client.IMsalMtlsHttpClientFactory.GetHttpClient(System.Security.Cryptography.X509Certificates.X509Certificate2 x509Certificate2) -> System.Net.Http.HttpClient Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync() -> System.Threading.Tasks.Task @@ -9,4 +13,4 @@ Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource.ImdsV2 = 8 -> Mi Microsoft.Identity.Client.ManagedIdentityApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> Microsoft.Identity.Client.ManagedIdentityApplicationBuilder static Microsoft.Identity.Client.ApplicationBase.ResetStateForTest() -> void Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2 -Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2.FormatResultAsync(Microsoft.Identity.Client.AuthenticationResult authenticationResult, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task \ No newline at end of file +Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2.FormatResultAsync(Microsoft.Identity.Client.AuthenticationResult authenticationResult, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt index c000207bbf..058ab31a1f 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt @@ -2,6 +2,10 @@ const Microsoft.Identity.Client.MsalError.CannotSwitchBetweenImdsVersionsForPrev const Microsoft.Identity.Client.MsalError.InvalidCertificate = "invalid_certificate" -> string const Microsoft.Identity.Client.MsalError.MtlsNotSupportedForManagedIdentity = "mtls_not_supported_for_managed_identity" -> string const Microsoft.Identity.Client.MsalError.MtlsPopTokenNotSupportedinImdsV1 = "mtls_pop_token_not_supported_in_imds_v1" -> string +Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2.ValidateCachedTokenAsync(Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData cachedTokenData) -> System.Threading.Tasks.Task +Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData +Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.MsalCacheValidationData() -> void +Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.PersistedCacheParameters.get -> System.Collections.Generic.IDictionary Microsoft.Identity.Client.IMsalMtlsHttpClientFactory Microsoft.Identity.Client.IMsalMtlsHttpClientFactory.GetHttpClient(System.Security.Cryptography.X509Certificates.X509Certificate2 x509Certificate2) -> System.Net.Http.HttpClient Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync() -> System.Threading.Tasks.Task @@ -9,4 +13,4 @@ Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource.ImdsV2 = 8 -> Mi Microsoft.Identity.Client.ManagedIdentityApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> Microsoft.Identity.Client.ManagedIdentityApplicationBuilder static Microsoft.Identity.Client.ApplicationBase.ResetStateForTest() -> void Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2 -Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2.FormatResultAsync(Microsoft.Identity.Client.AuthenticationResult authenticationResult, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task \ No newline at end of file +Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2.FormatResultAsync(Microsoft.Identity.Client.AuthenticationResult authenticationResult, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt index c000207bbf..058ab31a1f 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt @@ -2,6 +2,10 @@ const Microsoft.Identity.Client.MsalError.CannotSwitchBetweenImdsVersionsForPrev const Microsoft.Identity.Client.MsalError.InvalidCertificate = "invalid_certificate" -> string const Microsoft.Identity.Client.MsalError.MtlsNotSupportedForManagedIdentity = "mtls_not_supported_for_managed_identity" -> string const Microsoft.Identity.Client.MsalError.MtlsPopTokenNotSupportedinImdsV1 = "mtls_pop_token_not_supported_in_imds_v1" -> string +Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2.ValidateCachedTokenAsync(Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData cachedTokenData) -> System.Threading.Tasks.Task +Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData +Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.MsalCacheValidationData() -> void +Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.PersistedCacheParameters.get -> System.Collections.Generic.IDictionary Microsoft.Identity.Client.IMsalMtlsHttpClientFactory Microsoft.Identity.Client.IMsalMtlsHttpClientFactory.GetHttpClient(System.Security.Cryptography.X509Certificates.X509Certificate2 x509Certificate2) -> System.Net.Http.HttpClient Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync() -> System.Threading.Tasks.Task @@ -9,4 +13,4 @@ Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource.ImdsV2 = 8 -> Mi Microsoft.Identity.Client.ManagedIdentityApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> Microsoft.Identity.Client.ManagedIdentityApplicationBuilder static Microsoft.Identity.Client.ApplicationBase.ResetStateForTest() -> void Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2 -Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2.FormatResultAsync(Microsoft.Identity.Client.AuthenticationResult authenticationResult, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task \ No newline at end of file +Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2.FormatResultAsync(Microsoft.Identity.Client.AuthenticationResult authenticationResult, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt index c000207bbf..058ab31a1f 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt @@ -2,6 +2,10 @@ const Microsoft.Identity.Client.MsalError.CannotSwitchBetweenImdsVersionsForPrev const Microsoft.Identity.Client.MsalError.InvalidCertificate = "invalid_certificate" -> string const Microsoft.Identity.Client.MsalError.MtlsNotSupportedForManagedIdentity = "mtls_not_supported_for_managed_identity" -> string const Microsoft.Identity.Client.MsalError.MtlsPopTokenNotSupportedinImdsV1 = "mtls_pop_token_not_supported_in_imds_v1" -> string +Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2.ValidateCachedTokenAsync(Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData cachedTokenData) -> System.Threading.Tasks.Task +Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData +Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.MsalCacheValidationData() -> void +Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.PersistedCacheParameters.get -> System.Collections.Generic.IDictionary Microsoft.Identity.Client.IMsalMtlsHttpClientFactory Microsoft.Identity.Client.IMsalMtlsHttpClientFactory.GetHttpClient(System.Security.Cryptography.X509Certificates.X509Certificate2 x509Certificate2) -> System.Net.Http.HttpClient Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync() -> System.Threading.Tasks.Task @@ -9,4 +13,4 @@ Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource.ImdsV2 = 8 -> Mi Microsoft.Identity.Client.ManagedIdentityApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> Microsoft.Identity.Client.ManagedIdentityApplicationBuilder static Microsoft.Identity.Client.ApplicationBase.ResetStateForTest() -> void Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2 -Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2.FormatResultAsync(Microsoft.Identity.Client.AuthenticationResult authenticationResult, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task \ No newline at end of file +Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2.FormatResultAsync(Microsoft.Identity.Client.AuthenticationResult authenticationResult, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt index c000207bbf..058ab31a1f 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt @@ -2,6 +2,10 @@ const Microsoft.Identity.Client.MsalError.CannotSwitchBetweenImdsVersionsForPrev const Microsoft.Identity.Client.MsalError.InvalidCertificate = "invalid_certificate" -> string const Microsoft.Identity.Client.MsalError.MtlsNotSupportedForManagedIdentity = "mtls_not_supported_for_managed_identity" -> string const Microsoft.Identity.Client.MsalError.MtlsPopTokenNotSupportedinImdsV1 = "mtls_pop_token_not_supported_in_imds_v1" -> string +Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2.ValidateCachedTokenAsync(Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData cachedTokenData) -> System.Threading.Tasks.Task +Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData +Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.MsalCacheValidationData() -> void +Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.PersistedCacheParameters.get -> System.Collections.Generic.IDictionary Microsoft.Identity.Client.IMsalMtlsHttpClientFactory Microsoft.Identity.Client.IMsalMtlsHttpClientFactory.GetHttpClient(System.Security.Cryptography.X509Certificates.X509Certificate2 x509Certificate2) -> System.Net.Http.HttpClient Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync() -> System.Threading.Tasks.Task @@ -9,4 +13,4 @@ Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource.ImdsV2 = 8 -> Mi Microsoft.Identity.Client.ManagedIdentityApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> Microsoft.Identity.Client.ManagedIdentityApplicationBuilder static Microsoft.Identity.Client.ApplicationBase.ResetStateForTest() -> void Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2 -Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2.FormatResultAsync(Microsoft.Identity.Client.AuthenticationResult authenticationResult, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task \ No newline at end of file +Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2.FormatResultAsync(Microsoft.Identity.Client.AuthenticationResult authenticationResult, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task diff --git a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt index c000207bbf..058ab31a1f 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -2,6 +2,10 @@ const Microsoft.Identity.Client.MsalError.CannotSwitchBetweenImdsVersionsForPrev const Microsoft.Identity.Client.MsalError.InvalidCertificate = "invalid_certificate" -> string const Microsoft.Identity.Client.MsalError.MtlsNotSupportedForManagedIdentity = "mtls_not_supported_for_managed_identity" -> string const Microsoft.Identity.Client.MsalError.MtlsPopTokenNotSupportedinImdsV1 = "mtls_pop_token_not_supported_in_imds_v1" -> string +Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2.ValidateCachedTokenAsync(Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData cachedTokenData) -> System.Threading.Tasks.Task +Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData +Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.MsalCacheValidationData() -> void +Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.PersistedCacheParameters.get -> System.Collections.Generic.IDictionary Microsoft.Identity.Client.IMsalMtlsHttpClientFactory Microsoft.Identity.Client.IMsalMtlsHttpClientFactory.GetHttpClient(System.Security.Cryptography.X509Certificates.X509Certificate2 x509Certificate2) -> System.Net.Http.HttpClient Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync() -> System.Threading.Tasks.Task @@ -9,4 +13,4 @@ Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource.ImdsV2 = 8 -> Mi Microsoft.Identity.Client.ManagedIdentityApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> Microsoft.Identity.Client.ManagedIdentityApplicationBuilder static Microsoft.Identity.Client.ApplicationBase.ResetStateForTest() -> void Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2 -Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2.FormatResultAsync(Microsoft.Identity.Client.AuthenticationResult authenticationResult, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task \ No newline at end of file +Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2.FormatResultAsync(Microsoft.Identity.Client.AuthenticationResult authenticationResult, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/AuthenticationOperationTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/AuthenticationOperationTests.cs index fd041d1938..cb48b0ebb6 100644 --- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/AuthenticationOperationTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/AuthenticationOperationTests.cs @@ -25,6 +25,7 @@ using Microsoft.Identity.Test.Unit.BrokerTests; using Microsoft.VisualStudio.TestTools.UnitTesting; using NSubstitute; +using Microsoft.Identity.Client.Extensibility; namespace Microsoft.Identity.Test.Unit.PublicApiTests { @@ -210,6 +211,11 @@ public Task FormatResultAsync(AuthenticationResult authenticationResult, Cancell return Task.CompletedTask; } + public Task ValidateCachedTokenAsync(MsalCacheValidationData cachedTokenData) + { + return Task.FromResult(true); + } + public int TelemetryTokenType => 5; // Extension public string AccessTokenType => "bearer"; } @@ -301,5 +307,163 @@ private static async Task RunSilentCallAsync( return await builder .ExecuteAsync().ConfigureAwait(false); } + + [TestMethod] + public async Task ValidateCachedTokenAsync_WhenValidationFails_CacheIsIgnoredAsync() + { + // Arrange + var authScheme = Substitute.For(); + authScheme.AuthorizationHeaderPrefix.Returns("CustomToken"); + authScheme.AccessTokenType.Returns("bearer"); + authScheme.KeyId.Returns("keyid"); + authScheme.GetTokenRequestParams().Returns(new Dictionary() { { "tokenParam", "tokenParamValue" } }); + + // Setup FormatResultAsync to just add a prefix + authScheme.WhenForAnyArgs(x => x.FormatResultAsync(default, default)) + .Do(x => ((AuthenticationResult)x[0]).AccessToken = "validated_" + ((AuthenticationResult)x[0]).AccessToken); + + // Setup ValidateCachedTokenAsync to return false (cached token is invalid) + authScheme.ValidateCachedTokenAsync(Arg.Any()) + .Returns(Task.FromResult(false)); + + using (var httpManager = new MockHttpManager()) + { + httpManager.AddInstanceDiscoveryMockHandler(); + + var cca = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) + .WithExperimentalFeatures() + .WithClientSecret(TestConstants.ClientSecret) + .WithHttpManager(httpManager) + .BuildConcrete(); + + // First request - acquire initial token + httpManager.AddTokenResponse(TokenResponseType.Valid_ClientCredentials); + + var result1 = await cca.AcquireTokenForClient(TestConstants.s_scope) + .WithAuthenticationOperation(authScheme) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.IsNotNull(result1); + Assert.IsTrue(result1.AccessToken.StartsWith("validated_")); + Assert.AreEqual(TokenSource.IdentityProvider, result1.AuthenticationResultMetadata.TokenSource); + + // Second request - validation should fail, so new token should be acquired + httpManager.AddTokenResponse(TokenResponseType.Valid_ClientCredentials); + + var result2 = await cca.AcquireTokenForClient(TestConstants.s_scope) + .WithAuthenticationOperation(authScheme) + .ExecuteAsync() + .ConfigureAwait(false); + + // Assert - validation was called and failed, so a new token was acquired + await authScheme.Received(1).ValidateCachedTokenAsync( + Arg.Any()) + .ConfigureAwait(false); + + Assert.AreEqual(TokenSource.IdentityProvider, result2.AuthenticationResultMetadata.TokenSource); + Assert.AreEqual(0, httpManager.QueueSize); + } + } + + [TestMethod] + public async Task ValidateCachedTokenAsync_WhenValidationSucceeds_CacheIsUsedAsync() + { + // Arrange + var authScheme = Substitute.For(); + authScheme.AuthorizationHeaderPrefix.Returns("CustomToken"); + authScheme.AccessTokenType.Returns("bearer"); + authScheme.KeyId.Returns("keyid"); + authScheme.GetTokenRequestParams().Returns(new Dictionary() { { "tokenParam", "tokenParamValue" } }); + + // Setup FormatResultAsync + authScheme.WhenForAnyArgs(x => x.FormatResultAsync(default, default)) + .Do(x => ((AuthenticationResult)x[0]).AccessToken = "validated_" + ((AuthenticationResult)x[0]).AccessToken); + + // Setup ValidateCachedTokenAsync to return true (cached token is valid) + authScheme.ValidateCachedTokenAsync(Arg.Any()) + .Returns(Task.FromResult(true)); + + using (var httpManager = new MockHttpManager()) + { + httpManager.AddInstanceDiscoveryMockHandler(); + + var cca = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) + .WithExperimentalFeatures() + .WithClientSecret(TestConstants.ClientSecret) + .WithHttpManager(httpManager) + .BuildConcrete(); + + // First request - acquire initial token + httpManager.AddTokenResponse(TokenResponseType.Valid_ClientCredentials); + + var result1 = await cca.AcquireTokenForClient(TestConstants.s_scope) + .WithAuthenticationOperation(authScheme) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.IsNotNull(result1); + Assert.IsTrue(result1.AccessToken.StartsWith("validated_")); + Assert.AreEqual(TokenSource.IdentityProvider, result1.AuthenticationResultMetadata.TokenSource); + + // Second request - validation should succeed, so cached token should be returned + var result2 = await cca.AcquireTokenForClient(TestConstants.s_scope) + .WithAuthenticationOperation(authScheme) + .ExecuteAsync() + .ConfigureAwait(false); + + // Assert - validation was called and succeeded, so cached token was returned + await authScheme.Received(1).ValidateCachedTokenAsync( + Arg.Any()) + .ConfigureAwait(false); + + Assert.AreEqual(TokenSource.Cache, result2.AuthenticationResultMetadata.TokenSource); + Assert.AreEqual(result1.AccessToken, result2.AccessToken); + Assert.AreEqual(0, httpManager.QueueSize); // No additional token requests + } + } + + [TestMethod] + public async Task ValidateCachedTokenAsync_WithNullCachedItem_ValidationNotCalledAsync() + { + // Arrange + var authScheme = Substitute.For(); + authScheme.AuthorizationHeaderPrefix.Returns("CustomToken"); + authScheme.AccessTokenType.Returns("bearer"); + authScheme.KeyId.Returns("keyid"); + authScheme.GetTokenRequestParams().Returns(new Dictionary() { { "tokenParam", "tokenParamValue" } }); + + authScheme.WhenForAnyArgs(x => x.FormatResultAsync(default, default)) + .Do(x => ((AuthenticationResult)x[0]).AccessToken = "validated_" + ((AuthenticationResult)x[0]).AccessToken); + + authScheme.ValidateCachedTokenAsync(Arg.Any()) + .Returns(Task.FromResult(true)); + + using (var httpManager = new MockHttpManager()) + { + httpManager.AddInstanceDiscoveryMockHandler(); + + var cca = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) + .WithExperimentalFeatures() + .WithClientSecret(TestConstants.ClientSecret) + .WithHttpManager(httpManager) + .BuildConcrete(); + + // First request with empty cache - no cached token exists + httpManager.AddTokenResponse(TokenResponseType.Valid_ClientCredentials); + + var result = await cca.AcquireTokenForClient(TestConstants.s_scope) + .WithAuthenticationOperation(authScheme) + .ExecuteAsync() + .ConfigureAwait(false); + + // Assert - validation was never called because no cached token existed + await authScheme.DidNotReceive().ValidateCachedTokenAsync(Arg.Any()) + .ConfigureAwait(false); + + Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); + Assert.AreEqual(0, httpManager.QueueSize); + } + } } }