diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/ICertificatesObserver.cs b/src/Microsoft.Identity.Web.TokenAcquisition/ICertificatesObserver.cs index bedf48e89..616537bcd 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/ICertificatesObserver.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/ICertificatesObserver.cs @@ -26,6 +26,11 @@ public enum CerticateObserverAction /// happens when the STS does not accept the certificate any longer. /// Deselected, + + /// + /// The certificate was successfully used. + /// + SuccessfullyUsed, } /// diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt index 7dc5c5811..4afdc65b8 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +Microsoft.Identity.Web.Experimental.CerticateObserverAction.SuccessfullyUsed = 2 -> Microsoft.Identity.Web.Experimental.CerticateObserverAction diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt index 7dc5c5811..4afdc65b8 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +Microsoft.Identity.Web.Experimental.CerticateObserverAction.SuccessfullyUsed = 2 -> Microsoft.Identity.Web.Experimental.CerticateObserverAction diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Unshipped.txt index 7dc5c5811..4afdc65b8 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +Microsoft.Identity.Web.Experimental.CerticateObserverAction.SuccessfullyUsed = 2 -> Microsoft.Identity.Web.Experimental.CerticateObserverAction diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Unshipped.txt index 7dc5c5811..4afdc65b8 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +Microsoft.Identity.Web.Experimental.CerticateObserverAction.SuccessfullyUsed = 2 -> Microsoft.Identity.Web.Experimental.CerticateObserverAction diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 7dc5c5811..4afdc65b8 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +Microsoft.Identity.Web.Experimental.CerticateObserverAction.SuccessfullyUsed = 2 -> Microsoft.Identity.Web.Experimental.CerticateObserverAction diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs index 37f2a8e68..3d35b780f 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs @@ -171,14 +171,16 @@ public async Task AddAccountToCacheFromAuthorizationCodeAsyn _tokenAcquisitionHost.SetSession(Constants.SpaAuthCode, result.SpaAuthCode); } + NotifyCertificateSelection(mergedOptions, application, CerticateObserverAction.SuccessfullyUsed, null); + return new AcquireTokenResult( - result.AccessToken, - result.ExpiresOn, - result.TenantId, - result.IdToken, - result.Scopes, - result.CorrelationId, - result.TokenType); + result.AccessToken, + result.ExpiresOn, + result.TenantId, + result.IdToken, + result.Scopes, + result.CorrelationId, + result.TokenType); } catch (MsalServiceException exMsal) when (IsInvalidClientCertificateOrSignedAssertionError(exMsal)) { @@ -660,7 +662,9 @@ public async Task GetAuthenticationResultForAppAsync( try { - return await builder.ExecuteAsync(tokenAcquisitionOptions != null ? tokenAcquisitionOptions.CancellationToken : CancellationToken.None); + var result = await builder.ExecuteAsync(tokenAcquisitionOptions != null ? tokenAcquisitionOptions.CancellationToken : CancellationToken.None); + NotifyCertificateSelection(mergedOptions, application, CerticateObserverAction.SuccessfullyUsed, null); + return result; } catch (MsalServiceException exMsal) when (IsInvalidClientCertificateOrSignedAssertionError(exMsal)) { diff --git a/tests/E2E Tests/TokenAcquirerTests/CertificateRotationTest.cs b/tests/E2E Tests/TokenAcquirerTests/CertificateRotationTest.cs index 0106c94b8..98c24f981 100644 --- a/tests/E2E Tests/TokenAcquirerTests/CertificateRotationTest.cs +++ b/tests/E2E Tests/TokenAcquirerTests/CertificateRotationTest.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Concurrent; using System.Globalization; using System.Linq; using System.Security.Cryptography; @@ -29,6 +30,7 @@ public sealed class CertificateRotationTest : ICertificatesObserver ServicePrincipal? _servicePrincipal; readonly GraphServiceClient _graphServiceClient; X509Certificate? _currentCertificate = null; + ConcurrentDictionary _usages = []; public CertificateRotationTest() { @@ -111,9 +113,14 @@ public async Task TestCertificateRotationAsync() { } + // Ensure that the first certificate has a successful usage + Assert.True(_usages.TryGetValue(firstCertificate.Thumbprint, out int firstUsages)); + Assert.Equal(1, firstUsages); + // Keep acquiring tokens every minute for 5 mins // Tokens should be acquired successfully - for (int i = 0; i < 6; i++) + int i; + for (i = 0; i < 6; i++) { // Wait for a minute await Task.Delay(60 * 1000); @@ -139,6 +146,9 @@ public async Task TestCertificateRotationAsync() break; } + // Validate that the usages have incremented + Assert.True(_usages.TryGetValue(firstCertificate.Thumbprint, out firstUsages)); + Assert.Equal(i + 2, firstUsages); } catch (Exception) { @@ -147,9 +157,17 @@ public async Task TestCertificateRotationAsync() } } + // Check that the first certificate did not increment usages anymore. + Assert.True(_usages.TryGetValue(firstCertificate.Thumbprint, out firstUsages)); + Assert.Equal(i + 1, firstUsages); // Note that this is +1 instead of +2 as in the loop. + // Check the last certificate used is the second one. Assert.True(_currentCertificate != null && _currentCertificate.GetPublicKeyString() == secondCertificate.GetPublicKeyString()); + // Ensure that we have a recorded usage for the second cert + Assert.True(_usages.TryGetValue(firstCertificate.Thumbprint, out int secondUsage)); + Assert.Equal(1, secondUsage); + // Delete both certs from the cert store and remove the app registration await RemoveAppAndCertificatesAsync(firstCertificate, secondCertificate); } @@ -333,6 +351,11 @@ void ICertificatesObserver.OnClientCertificateChanged(CertificateChangeEventArg case CerticateObserverAction.Deselected: _currentCertificate = null; break; + + case CerticateObserverAction.SuccessfullyUsed: + _ = e.Certificate ?? throw new ArgumentNullException(nameof(e.Certificate)); + _usages[e.Certificate.Thumbprint] = _usages.TryGetValue(e.Certificate.Thumbprint, out int usages) ? usages + 1 : 0; + break; } } }