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 @@ -143,7 +143,7 @@ await Task.Run(async () =>
/// Regression test: Simulates the Agent Identities scenario where nested token acquisitions
/// with bad config should fail quickly, not hang.
/// </summary>
[Fact(Timeout = 5000)] // 5 second timeout - if it takes longer, it's likely hanging
[Fact(Timeout = 30000)] // 30 second timeout - generous to accommodate slow CI, but still validates no infinite hang
Comment thread
gladjohn marked this conversation as resolved.
public async Task GetAuthenticationResultForAppAsync_WithBadClientId_CompletesQuickly()
{
// Arrange
Comment thread
gladjohn marked this conversation as resolved.
Expand Down
46 changes: 18 additions & 28 deletions tests/Microsoft.Identity.Web.Test/CertificatesObserverTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,10 @@
// Licensed under the MIT License.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.ConstrainedExecution;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
Expand Down Expand Up @@ -53,8 +49,6 @@ static void RemoveCertificate(X509Certificate2? certificate)
string certName = $"CN=TestCert-{Guid.NewGuid():N}";
cert1 = CreateAndInstallCertificate(certName);

// Verify certificate is properly installed in store with timeout
await VerifyCertificateInStoreAsync(cert1, TimeSpan.FromSeconds(5));
var description = new CredentialDescription
{
SourceType = CredentialSource.StoreWithDistinguishedName,
Expand Down Expand Up @@ -134,9 +128,6 @@ static void RemoveCertificate(X509Certificate2? certificate)
RemoveCertificate(cert1);
cert2 = CreateAndInstallCertificate(certName);

// Verify certificate is properly installed in store with timeout
await VerifyCertificateInStoreAsync(cert2, TimeSpan.FromSeconds(5));

// Rerun but it fails this time
mockHttpFactory.ValidCertificates.Clear();
mockHttpFactory.ValidCertificates.Add(cert2);
Expand Down Expand Up @@ -184,7 +175,8 @@ internal X509Certificate2 CreateAndInstallCertificate(string certName)
var req = new CertificateRequest($"CN={certName}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
#endif

var cert = req.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddDays(1));
// Backdate NotBefore to avoid time-boundary races with FindByTimeValid.
var cert = req.CreateSelfSigned(DateTimeOffset.Now.AddMinutes(-5), DateTimeOffset.Now.AddDays(1));

byte[] bytes = cert.Export(X509ContentType.Pfx, (string?)null);
#pragma warning disable SYSLIB0057 // Type or member is obsolete
Expand All @@ -196,32 +188,30 @@ internal X509Certificate2 CreateAndInstallCertificate(string certName)
x509Store.Add(certWithPrivateKey);
x509Store.Close();

// X509Store.Add is synchronous — verify the cert is immediately findable
// using the same DN-based lookup that production code uses.
VerifyCertificateInStore(certWithPrivateKey);

return certWithPrivateKey;
}

/// <summary>
/// Verifies that a certificate is properly installed in the certificate store.
/// Verifies that a certificate is installed in the store using the same
/// distinguished-name lookup that the production credential loader uses.
/// X509Store.Add is synchronous, so no polling is needed.
/// </summary>
/// <param name="certificate">The certificate to verify.</param>
/// <param name="timeout">Maximum time to wait for the certificate to appear in the store.</param>
private static async Task VerifyCertificateInStoreAsync(X509Certificate2 certificate, TimeSpan timeout)
private static void VerifyCertificateInStore(X509Certificate2 certificate)
{
var stopwatch = Stopwatch.StartNew();
var minWaitTime = TimeSpan.FromSeconds(2); // Minimum wait to ensure store operations complete
do
using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadOnly);
var found = store.Certificates
.Find(X509FindType.FindBySubjectDistinguishedName, certificate.SubjectName.Name, false);
if (found.Count == 0)
{
using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadOnly);
var foundCerts = store.Certificates.Find(X509FindType.FindByThumbprint, certificate.Thumbprint, false);
if (foundCerts.Count > 0 && stopwatch.Elapsed >= minWaitTime)
{
return; // Certificate found and minimum wait time elapsed
}

await Task.Delay(100); // Wait 100ms before checking again
throw new InvalidOperationException(
$"Test setup failure: certificate '{certificate.SubjectName.Name}' (thumbprint {certificate.Thumbprint}) " +
$"was not found in CurrentUser/My immediately after Add. The store may be in an unexpected state.");
}
while (stopwatch.Elapsed < timeout);
throw new TimeoutException($"Certificate with thumbprint {certificate.Thumbprint} was not found in the certificate store within {timeout.TotalSeconds} seconds.");
}

private class TestCertificatesObserver : ICertificatesObserver
Expand Down
Loading