diff --git a/src/Microsoft.Identity.Web.Certificate/CertificateErrorMessage.cs b/src/Microsoft.Identity.Web.Certificate/CertificateErrorMessage.cs index 6e17ff181..b70aca88f 100644 --- a/src/Microsoft.Identity.Web.Certificate/CertificateErrorMessage.cs +++ b/src/Microsoft.Identity.Web.Certificate/CertificateErrorMessage.cs @@ -16,6 +16,9 @@ internal static class CertificateErrorMessage public const string BothClientSecretAndCertificateProvided = "IDW10105: Both client secret and client certificate, " + "cannot be included in the configuration of the web app when calling a web API. "; public const string ClientCertificatesHaveExpiredOrCannotBeLoaded = "IDW10109: All client certificates passed to the configuration have expired or can't be loaded. "; + public const string CustomProviderNameNullOrEmpty = "IDW10111 The name of the custom signed assertion provider is null or empty."; + public const string CustomProviderNotFound = "IDW10112: The custom signed assertion provider with name '{0}' was not found."; + public const string CustomProviderSourceLoaderNullOrEmpty = "IDW10113 The dictionary of SourceLoaders for custom signed assertion providers is null or empty."; // Encoding IDW10600 = "IDW10600:" public const string InvalidBase64UrlString = "IDW10601: Invalid Base64URL string. "; diff --git a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs new file mode 100644 index 000000000..b2d709ac8 --- /dev/null +++ b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Identity.Abstractions; + + +namespace Microsoft.Identity.Web +{ + public partial class DefaultCredentialsLoader + { + /// + /// Constructor for DefaultCredentialsLoader when using custom signed assertion provider source loaders. + /// + /// + /// Set of custom signed assertion providers. + public DefaultCredentialsLoader(ILogger? logger, IEnumerable customSignedAssertionProviders) : this(logger) + { + var sourceLoaderDict = new Dictionary(); + + foreach (var provider in customSignedAssertionProviders) + { + sourceLoaderDict.Add(provider.Name ?? provider.GetType().FullName!, provider); + } + + CustomSignedAssertionCredentialSourceLoaders = sourceLoaderDict; + } + + /// + /// Dictionary of custom signed assertion credential source loaders, by name (fully qualified type name). + /// + public IDictionary? CustomSignedAssertionCredentialSourceLoaders { get; } + + + private async Task ProcessCustomSignedAssertionAsync(CredentialDescription credentialDescription, CredentialSourceLoaderParameters? parameters) + { + // No source loader(s) + if (CustomSignedAssertionCredentialSourceLoaders == null || !CustomSignedAssertionCredentialSourceLoaders.Any()) + { + _logger.LogError(CertificateErrorMessage.CustomProviderSourceLoaderNullOrEmpty); + } + + // No provider name + else if (string.IsNullOrEmpty(credentialDescription.CustomSignedAssertionProviderName)) + { + _logger.LogError(CertificateErrorMessage.CustomProviderNameNullOrEmpty); + } + + // No source loader for provider name + else if (!CustomSignedAssertionCredentialSourceLoaders!.TryGetValue(credentialDescription.CustomSignedAssertionProviderName!, out ICredentialSourceLoader? sourceLoader)) + { + _logger.LogError(CertificateErrorMessage.CustomProviderNotFound, credentialDescription.CustomSignedAssertionProviderName); + } + + // Load the credentials, if there is an error, it is coming from the user's custom extension and should be logged and propagated. + else + { + try + { + await sourceLoader.LoadIfNeededAsync(credentialDescription, parameters); + } + catch (Exception ex) + { + Logger.CustomSignedAssertionProviderLoadingFailure(_logger, credentialDescription, ex); + throw; + } + return; + } + } + } +} diff --git a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.Logger.cs b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.Logger.cs index acb0fb2e4..3bbe7d35a 100644 --- a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.Logger.cs +++ b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.Logger.cs @@ -37,7 +37,7 @@ public static void CredentialLoadingFailure(ILogger logger, CredentialDescriptio public static void CustomSignedAssertionProviderLoadingFailure( ILogger logger, CredentialDescription cd, - CustomSignedAssertionProviderNotFoundException ex + Exception ex ) => s_customSignedAssertionProviderLoadingFailure(logger, cd.CustomSignedAssertionProviderName ?? "NameMissing", cd.SourceType.ToString(), cd.Skip, ex); } } diff --git a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoaderCustomSignedAssertion.cs b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoaderCustomSignedAssertion.cs deleted file mode 100644 index c706cdded..000000000 --- a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoaderCustomSignedAssertion.cs +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.Identity.Abstractions; - -namespace Microsoft.Identity.Web -{ - public partial class DefaultCredentialsLoader : ICredentialsLoader - { - /// - /// Constructor for DefaultCredentialsLoader when using custom signed assertion provider source loaders. - /// - /// - /// Set of custom signed assertion providers. - public DefaultCredentialsLoader(ILogger? logger, IEnumerable customSignedAssertionProviders) : this(logger) - { - var sourceLoaderDict = new Dictionary(); - - foreach (var provider in customSignedAssertionProviders) - { - sourceLoaderDict.Add(provider.Name ?? provider.GetType().FullName!, provider); - } - - CustomSignedAssertionCredentialSourceLoaders = sourceLoaderDict; - } - - /// - /// Dictionary of custom signed assertion credential source loaders, by name (fully qualified type name). - /// - public IDictionary? CustomSignedAssertionCredentialSourceLoaders { get; } - - private async Task ProcessCustomSignedAssertionAsync(CredentialDescription credentialDescription, CredentialSourceLoaderParameters? parameters) - { - CustomSignedAssertionProviderNotFoundException providerNotFoundException; - - // No source loader(s) - if (CustomSignedAssertionCredentialSourceLoaders == null || !CustomSignedAssertionCredentialSourceLoaders.Any()) - { - providerNotFoundException = CustomSignedAssertionProviderNotFoundException.SourceLoadersNullOrEmpty(); - } - - // No provider name - else if (string.IsNullOrEmpty(credentialDescription.CustomSignedAssertionProviderName)) - { - providerNotFoundException = CustomSignedAssertionProviderNotFoundException.ProviderNameNullOrEmpty(); - } - - // No source loader for provider name - else if (!CustomSignedAssertionCredentialSourceLoaders!.TryGetValue(credentialDescription.CustomSignedAssertionProviderName!, out ICredentialSourceLoader? sourceLoader)) - { - providerNotFoundException = CustomSignedAssertionProviderNotFoundException.ProviderNameNotFound(credentialDescription.CustomSignedAssertionProviderName!); - } - - // Load the credentials - else - { - await sourceLoader.LoadIfNeededAsync(credentialDescription, parameters); - return; - } - - Logger.CustomSignedAssertionProviderLoadingFailure(_logger, credentialDescription, providerNotFoundException); - throw providerNotFoundException; - } - } - - internal class CustomSignedAssertionProviderNotFoundException : Exception - { - private const string NameNullOrEmpty = "The name of the custom signed assertion provider is null or empty."; - private const string SourceLoaderNullOrEmpty = "The dictionary of SourceLoaders for custom signed assertion providers is null or empty."; - private const string ProviderNotFound = "The custom signed assertion provider with name '{0}' was not found."; - - public CustomSignedAssertionProviderNotFoundException(string message) : base(message) - { - } - - /// - /// Use when the SourceLoader library has entries, but the given name is not found. - /// - /// Name of custom signed assertion provider - /// An instance of this exception with a relevant message - public static CustomSignedAssertionProviderNotFoundException ProviderNameNotFound(string name) - { - return new CustomSignedAssertionProviderNotFoundException(message: string.Format(CultureInfo.InvariantCulture, ProviderNotFound, name)); - } - - /// - /// Use when the name of the custom signed assertion provider is null or empty. - /// - /// An instance of this exception with a relevant message - public static CustomSignedAssertionProviderNotFoundException ProviderNameNullOrEmpty() - { - return new CustomSignedAssertionProviderNotFoundException(NameNullOrEmpty); - } - - /// - /// Use when the SourceLoader library is null or empty. - /// - /// An instance of this exception with a relevant message - public static CustomSignedAssertionProviderNotFoundException SourceLoadersNullOrEmpty() - { - return new CustomSignedAssertionProviderNotFoundException(SourceLoaderNullOrEmpty); - } - } -} diff --git a/src/Microsoft.Identity.Web.Certificate/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.Certificate/InternalAPI.Unshipped.txt index 9815dc396..8ad8d88d1 100644 --- a/src/Microsoft.Identity.Web.Certificate/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.Certificate/InternalAPI.Unshipped.txt @@ -1,3 +1,6 @@ +const Microsoft.Identity.Web.CertificateErrorMessage.CustomProviderNameNullOrEmpty = "IDW10111 The name of the custom signed assertion provider is null or empty." -> string! +const Microsoft.Identity.Web.CertificateErrorMessage.CustomProviderNotFound = "IDW10112: The custom signed assertion provider with name '{0}' was not found." -> string! +const Microsoft.Identity.Web.CertificateErrorMessage.CustomProviderSourceLoaderNullOrEmpty = "IDW10113 The dictionary of SourceLoaders for custom signed assertion providers is null or empty." -> string! Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException.CustomSignedAssertionProviderNotFoundException(string! message) -> void static Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException.ProviderNameNotFound(string! name) -> Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException! diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 5e0813a88..92f8696cb 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -34,6 +34,7 @@ 2.22.0 6.0.12 1.48.0 + 4.20.72 2.2.4 5.0.3 diff --git a/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs b/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs index 696b15601..3ed503e57 100644 --- a/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs +++ b/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs @@ -3,8 +3,10 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Threading.Tasks; -using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Microsoft.Extensions.Logging; using Microsoft.Identity.Abstractions; using Xunit; @@ -12,22 +14,66 @@ namespace Microsoft.Identity.Web.Test { public class CustomSignedAssertionProviderTests { - [Theory] [MemberData(nameof(CustomSignedAssertionTestData))] - public async Task ProcessCustomSignedAssertionAsync_Tests(DefaultCredentialsLoader loader, CredentialDescription credentialDescription, Exception? expectedException = null) + public async Task ProcessCustomSignedAssertionAsync_Tests( + List providerList, + CredentialDescription credentialDescription, + string? expectedMessage = null) { + // Arrange + var loggedMessages = new List(); + var loggerMock = new Mock>(); + loggerMock.Setup(x => x.IsEnabled(It.IsAny())).Returns(true); + + var loader = new DefaultCredentialsLoader(loggerMock.Object, providerList); + + // Act try { await loader.LoadCredentialsIfNeededAsync(credentialDescription, null); + } catch (Exception ex) { - Assert.Equal(expectedException?.Message, ex.Message); + Assert.Equal(expectedMessage, ex.Message); + + // Haven't figured out yet how to get the mock logger to see the log coming from DefaultCredentialsLoader.Logger where it is logged using LogMessage.Define() + /* loggerMock.Verify( + x => + x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString()!.Contains(expectedMessage!)), + It.IsAny(), + It.IsAny>()), + Times.Once);*/ return; } - Assert.Null(expectedException); + // Assert + if (expectedMessage != null) + { + loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString()!.Contains(expectedMessage)), + It.IsAny(), + It.IsAny>()), + Times.Once); + } + else + { + loggerMock.Verify( + x => x.Log( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>()), + Times.Never); + } } public static IEnumerable CustomSignedAssertionTestData() @@ -35,59 +81,73 @@ public static IEnumerable CustomSignedAssertionTestData() // No source loaders yield return new object[] { - new DefaultCredentialsLoader(NullLogger.Instance, new List()), - new CredentialDescription { - CustomSignedAssertionProviderName = "Provider1", - SourceType = CredentialSource.CustomSignedAssertion - }, - CustomSignedAssertionProviderNotFoundException.SourceLoadersNullOrEmpty() + new List(), + new CredentialDescription + { + CustomSignedAssertionProviderName = "Provider1", + SourceType = CredentialSource.CustomSignedAssertion, + Skip = false + }, + CertificateErrorMessage.CustomProviderSourceLoaderNullOrEmpty }; - // No provider name + // No provider name given yield return new object[] { - new DefaultCredentialsLoader(NullLogger.Instance, new List { new CustomSignedAssertionProvider("Provider1") }), - new CredentialDescription - { - CustomSignedAssertionProviderName = null, - SourceType = CredentialSource.CustomSignedAssertion - }, - CustomSignedAssertionProviderNotFoundException.ProviderNameNullOrEmpty() + new List { new SuccessfulCustomSignedAssertionProvider("Provider2") }, + new CredentialDescription + { + CustomSignedAssertionProviderName = null, + SourceType = CredentialSource.CustomSignedAssertion + }, + CertificateErrorMessage.CustomProviderNameNullOrEmpty }; - // Provider name not found + // Given provider name not found yield return new object[] { - new DefaultCredentialsLoader(NullLogger.Instance, new List { new CustomSignedAssertionProvider("OtherProvider") }), - new CredentialDescription - { - CustomSignedAssertionProviderName = "Provider2", - SourceType = CredentialSource.CustomSignedAssertion - }, - CustomSignedAssertionProviderNotFoundException.ProviderNameNotFound("Provider2") + new List { new SuccessfulCustomSignedAssertionProvider("NotProvider3") }, + new CredentialDescription + { + CustomSignedAssertionProviderName = "Provider3", + SourceType = CredentialSource.CustomSignedAssertion + }, + string.Format(CultureInfo.InvariantCulture, CertificateErrorMessage.CustomProviderNotFound, "Provider3") }; - // Happy path + // Happy path (no logging expected) yield return new object[] { - new DefaultCredentialsLoader(NullLogger.Instance, new List { new CustomSignedAssertionProvider("Provider3") }), + new List { new SuccessfulCustomSignedAssertionProvider("Provider4") }, + new CredentialDescription + { + CustomSignedAssertionProviderName = "Provider4", + SourceType = CredentialSource.CustomSignedAssertion + } + }; + + // CustomSignedAssertionProvider (i.e. the user's extension) throws an exception + yield return new object[] + { + new List { new FailingCustomSignedAssertionProvider("Provider5") }, new CredentialDescription { - CustomSignedAssertionProviderName = "Provider3", + CustomSignedAssertionProviderName = "Provider5", SourceType = CredentialSource.CustomSignedAssertion - } + }, + FailingCustomSignedAssertionProvider.ExceptionMessage }; } - } - public class CustomSignedAssertionProvider : ICustomSignedAssertionProvider + // Helper class + internal class SuccessfulCustomSignedAssertionProvider : ICustomSignedAssertionProvider { public string Name { get; } public CredentialSource CredentialSource => CredentialSource.CustomSignedAssertion; - public CustomSignedAssertionProvider(string name) + public SuccessfulCustomSignedAssertionProvider(string name) { Name = name; } @@ -97,4 +157,22 @@ public Task LoadIfNeededAsync(CredentialDescription credentialDescription, Crede return Task.CompletedTask; } } + + internal class FailingCustomSignedAssertionProvider : ICustomSignedAssertionProvider + { + public string Name { get; } + public const string ExceptionMessage = "This extension is broken :("; + + public CredentialSource CredentialSource => CredentialSource.CustomSignedAssertion; + + public FailingCustomSignedAssertionProvider(string name) + { + Name = name; + } + + public Task LoadIfNeededAsync(CredentialDescription credentialDescription, CredentialSourceLoaderParameters? parameters) + { + throw new Exception("This extension is broken :("); + } + } } diff --git a/tests/Microsoft.Identity.Web.Test/Microsoft.Identity.Web.Test.csproj b/tests/Microsoft.Identity.Web.Test/Microsoft.Identity.Web.Test.csproj index 4b349e814..f11f922a3 100644 --- a/tests/Microsoft.Identity.Web.Test/Microsoft.Identity.Web.Test.csproj +++ b/tests/Microsoft.Identity.Web.Test/Microsoft.Identity.Web.Test.csproj @@ -26,6 +26,7 @@ +