Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
4 changes: 2 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,15 @@

<PropertyGroup Label="Common dependency versions">
<MicrosoftIdentityModelVersion Condition="'$(MicrosoftIdentityModelVersion)' == ''">8.14.0</MicrosoftIdentityModelVersion>
<MicrosoftIdentityClientVersion Condition="'$(MicrosoftIdentityClientVersion)' == ''">4.76.0</MicrosoftIdentityClientVersion>
<MicrosoftIdentityClientVersion Condition="'$(MicrosoftIdentityClientVersion)' == ''">4.77.0</MicrosoftIdentityClientVersion>
<MicrosoftIdentityAbstractionsVersion Condition="'$(MicrosoftIdentityAbstractionsVersion)' == ''">9.4.0</MicrosoftIdentityAbstractionsVersion>
<FxCopAnalyzersVersion>3.3.0</FxCopAnalyzersVersion>
<SystemTextEncodingsWebVersion>4.7.2</SystemTextEncodingsWebVersion>
<AzureSecurityKeyVaultSecretsVersion>4.6.0</AzureSecurityKeyVaultSecretsVersion>
<AzureIdentityVersion>1.11.4</AzureIdentityVersion>
<AzureSecurityKeyVaultCertificatesVersion>4.6.0</AzureSecurityKeyVaultCertificatesVersion>
<MicrosoftGraphVersion>4.36.0</MicrosoftGraphVersion>
<MicrosoftGraphBetaVersion>4.57.0-preview</MicrosoftGraphBetaVersion>
<MicrosoftIdentityAbstractionsVersion>9.3.0</MicrosoftIdentityAbstractionsVersion>
<!--CVE-2024-43485-->
<SystemTextJsonVersion>8.0.5</SystemTextJsonVersion>
<!--CVE-2023-29331-->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,30 @@ public DefaultCertificateLoader(IEnumerable<ICustomSignedAssertionProvider> cust
{
}

/// <summary>
/// Constructor with a logger and custom credential source loaders.
/// </summary>
/// <param name="credentialSourceLoaders">Additional credential source loaders. Can override built-in loaders.</param>
/// <param name="logger">Logger instance</param>
public DefaultCertificateLoader(
IEnumerable<ICredentialSourceLoader> credentialSourceLoaders,
ILogger<DefaultCertificateLoader>? logger) : base(credentialSourceLoaders, logger)
{
}

/// <summary>
/// Constructor with both custom signed assertion providers and custom credential source loaders.
/// </summary>
/// <param name="credentialSourceLoaders">Additional credential source loaders. Can override built-in loaders.</param>
/// <param name="customSignedAssertionProviders">List of providers of custom signed assertions</param>
/// <param name="logger">ILogger.</param>
public DefaultCertificateLoader(
IEnumerable<ICredentialSourceLoader> credentialSourceLoaders,
IEnumerable<ICustomSignedAssertionProvider> customSignedAssertionProviders,
ILogger<DefaultCertificateLoader>? logger) : base(customSignedAssertionProviders, logger, credentialSourceLoaders)
{
}

/// <summary>
/// This default is overridable at the level of the credential description (for the certificate from KeyVault).
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,40 @@ public DefaultCredentialsLoader(IEnumerable<ICustomSignedAssertionProvider> cust
CustomSignedAssertionCredentialSourceLoaders = sourceLoaderDict;
}

/// <summary>
/// Constructor for DefaultCredentialsLoader with both custom signed assertion providers and custom credential source loaders.
/// </summary>
/// <param name="customSignedAssertionProviders">Set of custom signed assertion providers.</param>
/// <param name="logger">ILogger.</param>
/// <param name="credentialSourceLoaders">Additional credential source loaders. Can override built-in loaders.</param>
public DefaultCredentialsLoader(
IEnumerable<ICustomSignedAssertionProvider> customSignedAssertionProviders,
ILogger<DefaultCredentialsLoader>? logger,
IEnumerable<ICredentialSourceLoader> credentialSourceLoaders) : this(credentialSourceLoaders, logger)
{
_ = Throws.IfNull(customSignedAssertionProviders);
var sourceLoaderDict = new Dictionary<string, ICustomSignedAssertionProvider>();

foreach (ICustomSignedAssertionProvider provider in customSignedAssertionProviders)
{
string providerName = provider.Name ?? provider.GetType().FullName!;
if (sourceLoaderDict.ContainsKey(providerName))
{
_logger.LogWarning(CertificateErrorMessage.CustomProviderNameAlreadyExists, providerName);
}
else
{
sourceLoaderDict.Add(providerName, provider);
}
}
CustomSignedAssertionCredentialSourceLoaders = sourceLoaderDict;
}

/// <summary>
/// Dictionary of custom signed assertion credential source loaders, by name (either ICustomSignedAssertionProvider.Name or the fully qualified type name).
/// The application can add more to process additional credential sources.
/// </summary>
protected IDictionary<string, ICustomSignedAssertionProvider>? CustomSignedAssertionCredentialSourceLoaders { get; }
protected internal /* internal for tests*/ IDictionary<string, ICustomSignedAssertionProvider>? CustomSignedAssertionCredentialSourceLoaders { get; }

private async Task ProcessCustomSignedAssertionAsync(CredentialDescription credentialDescription, CredentialSourceLoaderParameters? parameters)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,25 @@ public DefaultCredentialsLoader(ILogger<DefaultCredentialsLoader>? logger)
};
}

/// <summary>
/// Constructor with a logger and custom credential source loaders
/// </summary>
/// <param name="credentialSourceLoaders">Additional credential source loaders. Can override built-in loaders.</param>
/// <param name="logger">Logger instance</param>
public DefaultCredentialsLoader(
IEnumerable<ICredentialSourceLoader> credentialSourceLoaders,
ILogger<DefaultCredentialsLoader>? logger) : this(logger)
{
// Add additional/extensible loaders (can override built-ins)
if (credentialSourceLoaders != null)
{
foreach (var loader in credentialSourceLoaders)
{
CredentialSourceLoaders[loader.CredentialSource] = loader;
}
}
}

/// <summary>
/// Default constructor (for backward compatibility)
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
#nullable enable
Microsoft.Identity.Web.DefaultCertificateLoader.DefaultCertificateLoader(System.Collections.Generic.IEnumerable<Microsoft.Identity.Abstractions.ICredentialSourceLoader!>! credentialSourceLoaders, Microsoft.Extensions.Logging.ILogger<Microsoft.Identity.Web.DefaultCertificateLoader!>? logger) -> void
Microsoft.Identity.Web.DefaultCertificateLoader.DefaultCertificateLoader(System.Collections.Generic.IEnumerable<Microsoft.Identity.Abstractions.ICredentialSourceLoader!>! credentialSourceLoaders, System.Collections.Generic.IEnumerable<Microsoft.Identity.Abstractions.ICustomSignedAssertionProvider!>! customSignedAssertionProviders, Microsoft.Extensions.Logging.ILogger<Microsoft.Identity.Web.DefaultCertificateLoader!>? logger) -> void
Microsoft.Identity.Web.DefaultCredentialsLoader.DefaultCredentialsLoader(System.Collections.Generic.IEnumerable<Microsoft.Identity.Abstractions.ICredentialSourceLoader!>! credentialSourceLoaders, Microsoft.Extensions.Logging.ILogger<Microsoft.Identity.Web.DefaultCredentialsLoader!>? logger) -> void
Microsoft.Identity.Web.DefaultCredentialsLoader.DefaultCredentialsLoader(System.Collections.Generic.IEnumerable<Microsoft.Identity.Abstractions.ICustomSignedAssertionProvider!>! customSignedAssertionProviders, Microsoft.Extensions.Logging.ILogger<Microsoft.Identity.Web.DefaultCredentialsLoader!>? logger, System.Collections.Generic.IEnumerable<Microsoft.Identity.Abstractions.ICredentialSourceLoader!>! credentialSourceLoaders) -> void
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
using Microsoft.Extensions.Options;
using Microsoft.Identity.Abstractions;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Advanced;
using Microsoft.Identity.Client.Extensibility;
using Microsoft.Identity.Web.Experimental;
using Microsoft.Identity.Web.TestOnly;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@
// Licensed under the MIT License.

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Identity.Abstractions;
using Microsoft.Identity.Web.Test.Common;
using Microsoft.Identity.Web.TokenCacheProviders.InMemory;
using Xunit;

namespace Microsoft.Identity.Web.Test.Certificates
Expand Down Expand Up @@ -56,6 +63,30 @@ public void TestDefaultCertificateLoader(CertificateSource certificateSource, st
}

[Fact]
public async Task TextExtensibilityE2E()
{
ServiceCollection services = new ServiceCollection();
services.AddTokenAcquisition();
services.AddHttpClient();
services.AddInMemoryTokenCaches();
services.TryAddEnumerable(ServiceDescriptor.Singleton<ICredentialSourceLoader>(new DefaultCertificateLoaderTests.MockCredentialSourceLoader(CredentialSource.ManagedCertificate, "e2e-mock")));

ServiceProvider serviceProvider = services.BuildServiceProvider();
var credentialLoader = serviceProvider.GetRequiredService<ICredentialsLoader>();

CredentialDescription? cd = await credentialLoader.LoadFirstValidCredentialsAsync(new List<CredentialDescription>
{
new CredentialDescription
{
SourceType = CredentialSource.ManagedCertificate
}
});

Assert.Equal("e2e-mock", cd?.CachedValue?.ToString());

}

[Fact]
public void TestLoadFirstCertificate()
{
IEnumerable<CertificateDescription> certDescriptions = [CertificateDescription.FromBase64Encoded(TestConstants.CertificateX5c)];
Expand Down Expand Up @@ -110,5 +141,210 @@ public void TestLoadCertificateWithPrivateKey(
Assert.NotNull(certificateDescription.Certificate);
Assert.True(certificateDescription.Certificate.HasPrivateKey);
}

[Fact]
public void TestDefaultCredentialsLoaderWithCustomLoaders()
{
// Arrange
var customLoaders = new List<ICredentialSourceLoader>
{
new MockCredentialSourceLoader(CredentialSource.Base64Encoded, "custom-mock")
};

// Act
var loader = new DefaultCredentialsLoader(customLoaders, null);

// Assert
Assert.NotNull(loader.CredentialSourceLoaders);
Assert.True(loader.CredentialSourceLoaders.ContainsKey(CredentialSource.Base64Encoded));

// Verify the custom loader overrode the built-in one
var customLoader = loader.CredentialSourceLoaders[CredentialSource.Base64Encoded] as MockCredentialSourceLoader;
Assert.NotNull(customLoader);
Assert.Equal("custom-mock", customLoader.TestValue);
}

[Fact]
public void TestDefaultCertificateLoaderWithCustomLoaders()
{
// Arrange
var customLoaders = new List<ICredentialSourceLoader>
{
new MockCredentialSourceLoader(CredentialSource.Path, "certificate-mock")
};

// Act
var loader = new DefaultCertificateLoader(customLoaders, null);

// Assert
Assert.NotNull(loader.CredentialSourceLoaders);
Assert.True(loader.CredentialSourceLoaders.ContainsKey(CredentialSource.Path));

// Verify the custom loader overrode the built-in one
var customLoader = loader.CredentialSourceLoaders[CredentialSource.Path] as MockCredentialSourceLoader;
Assert.NotNull(customLoader);
Assert.Equal("certificate-mock", customLoader.TestValue);
}

[Fact]
public async Task TestCustomLoaderIsUsed()
{
// Arrange
var customLoaders = new List<ICredentialSourceLoader>
{
new MockCredentialSourceLoader(CredentialSource.StoreWithThumbprint, "used-custom-loader")
};
var loader = new DefaultCredentialsLoader(customLoaders, null);
var credentialDescription = new CredentialDescription
{
SourceType = CredentialSource.StoreWithThumbprint
};

// Act
await loader.LoadCredentialsIfNeededAsync(credentialDescription);

// Assert
Assert.Equal("used-custom-loader", credentialDescription.CachedValue);
}

[Fact]
public void TestConstructorWithNullCustomLoaders()
{
// Arrange & Act
var loader = new DefaultCredentialsLoader(null);

// Assert - should still have built-in loaders
Assert.NotNull(loader.CredentialSourceLoaders);
Assert.True(loader.CredentialSourceLoaders.ContainsKey(CredentialSource.KeyVault));
Assert.True(loader.CredentialSourceLoaders.ContainsKey(CredentialSource.Base64Encoded));
}

[Fact]
public void TestBackwardCompatibilityExistingConstructors()
{
// Test that existing constructors still work
var loader1 = new DefaultCredentialsLoader();
var loader2 = new DefaultCredentialsLoader(null);
var loader3 = new DefaultCertificateLoader();
var loader4 = new DefaultCertificateLoader(null);

// All should have built-in loaders
Assert.NotNull(loader1.CredentialSourceLoaders);
Assert.NotNull(loader2.CredentialSourceLoaders);
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);
}

[Fact]
public void TestCustomLoaderWithNonConflictingCredentialSources()
{
// Arrange - Use a custom loader that doesn't override any built-in loader
var customLoaders = new List<ICredentialSourceLoader>
{
new MockCredentialSourceLoader(CredentialSource.Certificate, "custom-certificate")
};

// Act
var loader = new DefaultCredentialsLoader(customLoaders, null);

// 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

// Verify built-in loaders are still present
Assert.True(loader.CredentialSourceLoaders.ContainsKey(CredentialSource.KeyVault));
Assert.True(loader.CredentialSourceLoaders.ContainsKey(CredentialSource.Base64Encoded));
Assert.True(loader.CredentialSourceLoaders.ContainsKey(CredentialSource.Path));

// Verify custom loader is added
Assert.True(loader.CredentialSourceLoaders.ContainsKey(CredentialSource.Certificate));
var customLoader = loader.CredentialSourceLoaders[CredentialSource.Certificate] as MockCredentialSourceLoader;
Assert.NotNull(customLoader);
Assert.Equal("custom-certificate", customLoader.TestValue);
}

[Fact]
public void TestConstructorWithBothCustomSignedAssertionProvidersAndCredentialSourceLoaders()
{
// Arrange
var customSignedAssertionProviders = new List<ICustomSignedAssertionProvider>
{
new MockCustomSignedAssertionProvider("test-provider")
};
var customLoaders = new List<ICredentialSourceLoader>
{
new MockCredentialSourceLoader(CredentialSource.Path, "combined-test")
};

// Act - Test DefaultCredentialsLoader comprehensive constructor
var credentialsLoader = new DefaultCredentialsLoader(customSignedAssertionProviders, null, customLoaders);

// Assert
Assert.NotNull(credentialsLoader.CredentialSourceLoaders);
Assert.NotNull(credentialsLoader.CustomSignedAssertionCredentialSourceLoaders);

// Verify custom credential source loader is present
Assert.True(credentialsLoader.CredentialSourceLoaders.ContainsKey(CredentialSource.Path));
var customLoader = credentialsLoader.CredentialSourceLoaders[CredentialSource.Path] as MockCredentialSourceLoader;
Assert.NotNull(customLoader);
Assert.Equal("combined-test", customLoader.TestValue);

// Verify custom signed assertion provider is present
Assert.True(credentialsLoader.CustomSignedAssertionCredentialSourceLoaders.ContainsKey("test-provider"));

// Act - Test DefaultCertificateLoader comprehensive constructor
var certificateLoader = new DefaultCertificateLoader(customLoaders, customSignedAssertionProviders, null);

// Assert
Assert.NotNull(certificateLoader.CredentialSourceLoaders);
Assert.NotNull(certificateLoader.CustomSignedAssertionCredentialSourceLoaders);
}

/// <summary>
/// Mock credential source loader for testing
/// </summary>
internal class MockCredentialSourceLoader : ICredentialSourceLoader
{
public CredentialSource CredentialSource { get; }
public string TestValue { get; }

public MockCredentialSourceLoader(CredentialSource credentialSource, string testValue = "mock")
{
CredentialSource = credentialSource;
TestValue = testValue;
}

public Task LoadIfNeededAsync(CredentialDescription credentialDescription, CredentialSourceLoaderParameters? parameters = null)
{
// Mock implementation - just mark that this loader was used
credentialDescription.CachedValue = TestValue;
return Task.CompletedTask;
}
}

/// <summary>
/// Mock custom signed assertion provider for testing
/// </summary>
internal class MockCustomSignedAssertionProvider : ICustomSignedAssertionProvider
{
public CredentialSource CredentialSource => CredentialSource.CustomSignedAssertion;
public string Name { get; }

public MockCustomSignedAssertionProvider(string name)
{
Name = name;
}

public Task LoadIfNeededAsync(CredentialDescription credentialDescription, CredentialSourceLoaderParameters? parameters = null)
{
credentialDescription.CachedValue = $"mock-assertion-{Name}";
return Task.CompletedTask;
}
}
}
}
Loading