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
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
<PropertyGroup Label="Common dependency versions">
<MicrosoftIdentityModelVersion Condition="'$(MicrosoftIdentityModelVersion)' == ''">8.15.0</MicrosoftIdentityModelVersion>
<MicrosoftIdentityClientVersion Condition="'$(MicrosoftIdentityClientVersion)' == ''">4.82.0</MicrosoftIdentityClientVersion>
<MicrosoftIdentityAbstractionsVersion Condition="'$(MicrosoftIdentityAbstractionsVersion)' == ''">11.0.0</MicrosoftIdentityAbstractionsVersion>
<MicrosoftIdentityAbstractionsVersion Condition="'$(MicrosoftIdentityAbstractionsVersion)' == ''">11.1.0</MicrosoftIdentityAbstractionsVersion>
<FxCopAnalyzersVersion>3.3.0</FxCopAnalyzersVersion>
<SystemTextEncodingsWebVersion>4.7.2</SystemTextEncodingsWebVersion>
<AzureSecurityKeyVaultSecretsVersion>4.6.0</AzureSecurityKeyVaultSecretsVersion>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,29 @@ public static CertificateDescription FromStoreWithDistinguishedName(
};
}

/// <summary>
/// Creates a certificate description from a certificate subject name (such as "MyCert")
/// and store location (Certificate Manager on Windows, for instance).
/// The provided subject name is matched as a substring against the Subject field of each
/// certificate in the store; the most recently issued matching certificate is selected.
/// </summary>
/// <param name="certificateSubjectName">Certificate subject name (or substring of the subject) to search for in the store.</param>
/// <param name="certificateStoreLocation">Store location where to find the certificate.</param>
/// <param name="certificateStoreName">Store name where to find the certificate.</param>
/// <returns>A certificate description.</returns>
public static CertificateDescription FromStoreWithSubjectName(
string certificateSubjectName,
StoreLocation certificateStoreLocation = StoreLocation.CurrentUser,
StoreName certificateStoreName = StoreName.My)
{
return new CertificateDescription
{
SourceType = CertificateSource.StoreWithSubjectName,
CertificateStorePath = $"{certificateStoreLocation}/{certificateStoreName}",
CertificateSubjectName = certificateSubjectName,
};
}

/// <summary>
/// Defines where and how to import the private key of an X.509 certificate.
/// </summary>
Expand Down
6 changes: 6 additions & 0 deletions src/Microsoft.Identity.Web.Certificate/CertificateSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,11 @@ public enum CertificateSource
/// From the certificate store, described by its distinguished name.
/// </summary>
StoreWithDistinguishedName = 5,

/// <summary>
/// From the certificate store, described by its subject name.
/// The provided value is matched as a substring against the certificate's Subject field.
/// </summary>
StoreWithSubjectName = 13,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public DefaultCredentialsLoader(ILogger<DefaultCredentialsLoader>? logger)
{ CredentialSource.Path, new FromPathCertificateLoader() },
{ CredentialSource.StoreWithThumbprint, new StoreWithThumbprintCertificateLoader() },
{ CredentialSource.StoreWithDistinguishedName, new StoreWithDistinguishedNameCertificateLoader() },
{ CredentialSource.StoreWithSubjectName, new StoreWithSubjectNameCertificateLoader() },
{ CredentialSource.Base64Encoded, new Base64EncodedCertificateLoader() },
{ CredentialSource.SignedAssertionFromManagedIdentity, new SignedAssertionFromManagedIdentityCredentialLoader(_logger) },
{ CredentialSource.SignedAssertionFilePath, new SignedAssertionFilePathCredentialsLoader(_logger) }
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
#nullable enable
Microsoft.Identity.Web.StoreWithSubjectNameCertificateLoader
Microsoft.Identity.Web.StoreWithSubjectNameCertificateLoader.CredentialSource.get -> Microsoft.Identity.Abstractions.CredentialSource
Microsoft.Identity.Web.StoreWithSubjectNameCertificateLoader.LoadIfNeededAsync(Microsoft.Identity.Abstractions.CredentialDescription! credentialDescription, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? _) -> System.Threading.Tasks.Task!
Microsoft.Identity.Web.StoreWithSubjectNameCertificateLoader.StoreWithSubjectNameCertificateLoader() -> void
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#nullable enable
Microsoft.Identity.Web.CertificateSource.StoreWithSubjectName = 13 -> Microsoft.Identity.Web.CertificateSource
static Microsoft.Identity.Web.CertificateDescription.FromStoreWithSubjectName(string! certificateSubjectName, System.Security.Cryptography.X509Certificates.StoreLocation certificateStoreLocation = System.Security.Cryptography.X509Certificates.StoreLocation.CurrentUser, System.Security.Cryptography.X509Certificates.StoreName certificateStoreName = System.Security.Cryptography.X509Certificates.StoreName.My) -> Microsoft.Identity.Web.CertificateDescription!
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using Microsoft.Identity.Abstractions;

namespace Microsoft.Identity.Web
{
internal sealed class StoreWithSubjectNameCertificateLoader : ICredentialSourceLoader
{
public CredentialSource CredentialSource => CredentialSource.StoreWithSubjectName;

public Task LoadIfNeededAsync(CredentialDescription credentialDescription, CredentialSourceLoaderParameters? _)
{
credentialDescription.Certificate = LoadFromStoreWithSubjectName(
credentialDescription.CertificateSubjectName!,
credentialDescription.CertificateStorePath!);
credentialDescription.CachedValue = credentialDescription.Certificate;
return Task.CompletedTask;
}

private static X509Certificate2? LoadFromStoreWithSubjectName(
string certificateSubjectName,
string storeDescription = CertificateConstants.PersonalUserCertificateStorePath)
{
StoreLocation certificateStoreLocation = StoreLocation.CurrentUser;
StoreName certificateStoreName = StoreName.My;
CertificateLoaderHelper.ParseStoreLocationAndName(
storeDescription,
ref certificateStoreLocation,
ref certificateStoreName);

X509Certificate2? cert;
using (X509Store x509Store = new X509Store(
certificateStoreName,
certificateStoreLocation))
{
cert = CertificateLoaderHelper.FindCertificateByCriterium(
x509Store,
X509FindType.FindBySubjectName,
certificateSubjectName);
}

return cert;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ public void TestFromStoreWithThumbprint(string certificateThumbprint, StoreLocat
Assert.Equal(certificateThumbprint, certificateDescription.CertificateThumbprint);
}

[Theory]
[InlineData("TestCert", StoreLocation.LocalMachine, StoreName.Root)]
public void TestFromStoreWithSubjectName(string certificateSubjectName, StoreLocation storeLocation, StoreName storeName)
{
CertificateDescription certificateDescription =
CertificateDescription.FromStoreWithSubjectName(certificateSubjectName, storeLocation, storeName);
Assert.Equal(CertificateSource.StoreWithSubjectName, certificateDescription.SourceType);
Assert.Equal(certificateSubjectName, certificateDescription.CertificateSubjectName);
Assert.Equal($"{storeLocation}/{storeName}", certificateDescription.CertificateStorePath);
}

[Fact]
public void TestFromCertificate()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class DefaultCertificateLoaderTests
// [InlineData(CertificateSource.Path, @"c:\temp\WebAppCallingWebApiCert.pfx", "")]
// [InlineData(CertificateSource.StoreWithDistinguishedName, "CurrentUser/My", "CN=WebAppCallingWebApiCert")]
// [InlineData(CertificateSource.StoreWithThumbprint, "CurrentUser/My", "962D129A859174EE8B5596985BD18EFEB6961684")]
// [InlineData(CertificateSource.StoreWithSubjectName, "CurrentUser/My", "MyCertificate")]
#pragma warning disable xUnit1012 // Null should only be used for nullable parameters
[InlineData(CertificateSource.Base64Encoded, null, TestConstants.CertificateX5c)]
#pragma warning restore xUnit1012 // Null should only be used for nullable parameters
Expand Down Expand Up @@ -51,6 +52,10 @@ public void TestDefaultCertificateLoader(CertificateSource certificateSource, st
certificateDescription.CertificateDistinguishedName = referenceOrValue;
certificateDescription.CertificateStorePath = container;
break;
case CertificateSource.StoreWithSubjectName:
certificateDescription = CertificateDescription.FromStoreWithSubjectName(referenceOrValue);
certificateDescription.CertificateStorePath = container;
break;
default:
certificateDescription = new CertificateDescription();
break;
Expand Down Expand Up @@ -234,10 +239,10 @@ public void TestBackwardCompatibilityExistingConstructors()
Assert.NotNull(loader3.CredentialSourceLoaders);
Assert.NotNull(loader4.CredentialSourceLoaders);

Assert.True(loader1.CredentialSourceLoaders.Count >= 7); // Should have at least 7 built-in loaders
Assert.True(loader2.CredentialSourceLoaders.Count >= 7);
Assert.True(loader3.CredentialSourceLoaders.Count >= 7);
Assert.True(loader4.CredentialSourceLoaders.Count >= 7);
Assert.True(loader1.CredentialSourceLoaders.Count >= 8); // Should have at least 8 built-in loaders
Assert.True(loader2.CredentialSourceLoaders.Count >= 8);
Assert.True(loader3.CredentialSourceLoaders.Count >= 8);
Assert.True(loader4.CredentialSourceLoaders.Count >= 8);
}

[Fact]
Expand All @@ -254,7 +259,7 @@ public void TestCustomLoaderWithNonConflictingCredentialSources()

// Assert - should have all built-in loaders plus the custom one
Assert.NotNull(loader.CredentialSourceLoaders);
Assert.True(loader.CredentialSourceLoaders.Count >= 8); // 7 built-in + 1 custom
Assert.True(loader.CredentialSourceLoaders.Count >= 9); // 8 built-in + 1 custom

// Verify built-in loaders are still present
Assert.True(loader.CredentialSourceLoaders.ContainsKey(CredentialSource.KeyVault));
Expand Down Expand Up @@ -305,6 +310,66 @@ public void TestConstructorWithBothCustomSignedAssertionProvidersAndCredentialSo
Assert.NotNull(certificateLoader.CustomSignedAssertionCredentialSourceLoaders);
}

[Fact]
public void TestFromStoreWithSubjectNameCreatesCorrectDescription()
{
// Arrange & Act
var description = CertificateDescription.FromStoreWithSubjectName(
"MyCertificate",
StoreLocation.LocalMachine,
StoreName.My);

// Assert
Assert.Equal(CertificateSource.StoreWithSubjectName, description.SourceType);
Assert.Equal("MyCertificate", description.CertificateSubjectName);
Assert.Equal("LocalMachine/My", description.CertificateStorePath);
}

[Fact]
public void TestFromStoreWithSubjectNameDefaultStoreLocation()
{
// Arrange & Act
var description = CertificateDescription.FromStoreWithSubjectName("MyCertificate");

// Assert
Assert.Equal(CertificateSource.StoreWithSubjectName, description.SourceType);
Assert.Equal("MyCertificate", description.CertificateSubjectName);
Assert.Equal("CurrentUser/My", description.CertificateStorePath);
}

[Fact]
public void TestStoreWithSubjectNameLoaderIsRegistered()
{
// Act
var loader = new DefaultCredentialsLoader();

// Assert
Assert.True(loader.CredentialSourceLoaders.ContainsKey(CredentialSource.StoreWithSubjectName));
}

[Fact]
public async Task TestStoreWithSubjectNameLoaderIsUsed()
{
// Arrange
var customLoaders = new List<ICredentialSourceLoader>
{
new MockCredentialSourceLoader(CredentialSource.StoreWithSubjectName, "subject-name-mock")
};
var loader = new DefaultCredentialsLoader(customLoaders, null);
var credentialDescription = new CredentialDescription
{
SourceType = CredentialSource.StoreWithSubjectName,
CertificateSubjectName = "MyCertificate",
CertificateStorePath = CertificateConstants.PersonalUserCertificateStorePath
};

// Act
await loader.LoadCredentialsIfNeededAsync(credentialDescription);

// Assert
Assert.Equal("subject-name-mock", credentialDescription.CachedValue);
}

/// <summary>
/// Mock credential source loader for testing
/// </summary>
Expand Down
Loading