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;
}
}
}