diff --git a/Microsoft.Identity.Web.sln b/Microsoft.Identity.Web.sln index 90eb6ff9d..a86b47f71 100644 --- a/Microsoft.Identity.Web.sln +++ b/Microsoft.Identity.Web.sln @@ -160,6 +160,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Web.UI", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Web.AotCompatibility.TestApp", "tests\Microsoft.Identity.Web.AotCompatibility.TestApp\Microsoft.Identity.Web.AotCompatibility.TestApp.csproj", "{BCE63265-6D36-423A-9C3D-BF8E448C7EA0}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CustomSignedAssertionProviderTests", "tests\E2E Tests\CustomSignedAssertionProviderTests\CustomSignedAssertionProviderTests.csproj", "{A390650C-BCE1-4CB3-8C97-9EF9CFF5B7C5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -370,6 +372,10 @@ Global {BCE63265-6D36-423A-9C3D-BF8E448C7EA0}.Debug|Any CPU.Build.0 = Debug|Any CPU {BCE63265-6D36-423A-9C3D-BF8E448C7EA0}.Release|Any CPU.ActiveCfg = Release|Any CPU {BCE63265-6D36-423A-9C3D-BF8E448C7EA0}.Release|Any CPU.Build.0 = Release|Any CPU + {A390650C-BCE1-4CB3-8C97-9EF9CFF5B7C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A390650C-BCE1-4CB3-8C97-9EF9CFF5B7C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A390650C-BCE1-4CB3-8C97-9EF9CFF5B7C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A390650C-BCE1-4CB3-8C97-9EF9CFF5B7C5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -442,6 +448,7 @@ Global {4A63EA63-5679-4498-BB4C-30E09F268E00} = {E37CDBC1-18F6-4C06-A3EE-532C9106721F} {C6CB0D5B-917A-4127-9984-7592C757BBDE} = {1DDE1AAC-5AE6-4725-94B6-A26C58D3423F} {BCE63265-6D36-423A-9C3D-BF8E448C7EA0} = {B4E72F1C-603F-437C-AAA1-153A604CD34A} + {A390650C-BCE1-4CB3-8C97-9EF9CFF5B7C5} = {45B20A78-91F8-4DD2-B9AD-F12D3A93536C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {104367F1-CE75-4F40-B32F-F14853973187} diff --git a/src/Microsoft.Identity.Web.Certificate/CertificateErrorMessage.cs b/src/Microsoft.Identity.Web.Certificate/CertificateErrorMessage.cs index 6e17ff181..f2e370029 100644 --- a/src/Microsoft.Identity.Web.Certificate/CertificateErrorMessage.cs +++ b/src/Microsoft.Identity.Web.Certificate/CertificateErrorMessage.cs @@ -10,12 +10,17 @@ internal static class CertificateErrorMessage { // Configuration IDW10100 = "IDW10100:" public const string ClientSecretAndCertificateNull = - "IDW10104: Both client secret and client certificate cannot be null or whitespace, " + - "and only ONE must be included in the configuration of the web app when calling a web API. " + - "For instance, in the appsettings.json file. "; - 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. "; + "IDW10104: Both client secret and client certificate cannot be null or whitespace, " + + "and only ONE must be included in the configuration of the web app when calling a web API. " + + "For instance, in the appsettings.json file. "; + 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 CustomProviderNameAlreadyExists = + "IDW10111 The custom signed assertion provider '{0}' already exists, only the the first instance of ICustomSignedAssertionProvider with this name will be used."; + public const string CustomProviderNameNullOrEmpty = "IDW10112 The name of the custom signed assertion provider is null or empty."; + public const string CustomProviderNotFound = "IDW10113: The custom signed assertion provider with name '{0}' was not found. Was it registered in the service collection?"; + public const string CustomProviderSourceLoaderNullOrEmpty = "IDW10114 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/DefaultCertificateLoader.cs b/src/Microsoft.Identity.Web.Certificate/DefaultCertificateLoader.cs index 4d5c2a527..46527fd2a 100644 --- a/src/Microsoft.Identity.Web.Certificate/DefaultCertificateLoader.cs +++ b/src/Microsoft.Identity.Web.Certificate/DefaultCertificateLoader.cs @@ -43,6 +43,15 @@ public DefaultCertificateLoader() : this(null) { } + /// + /// Constructor with custom signed assertion providers. + /// + /// List of providers of custom signed assertions + /// ILogger. + public DefaultCertificateLoader(IEnumerable customSignedAssertionProviders, ILogger? logger) : base(customSignedAssertionProviders, logger) + { + } + /// /// This default is overridable at the level of the credential description (for the certificate from KeyVault). /// 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..2d9a22fd9 --- /dev/null +++ b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Globalization; +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. + /// ILogger. + public DefaultCredentialsLoader(IEnumerable customSignedAssertionProviders, ILogger? logger) : this(logger) + { + _ = Throws.IfNull(customSignedAssertionProviders); + var sourceLoaderDict = new Dictionary(); + + 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; + } + + /// + /// 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. + /// + protected IDictionary? CustomSignedAssertionCredentialSourceLoaders { get; } + + private async Task ProcessCustomSignedAssertionAsync(CredentialDescription credentialDescription, CredentialSourceLoaderParameters? parameters) + { + if (CustomSignedAssertionCredentialSourceLoaders == null || CustomSignedAssertionCredentialSourceLoaders.Count == 0) + { + // No source loader(s) + _logger.LogError(CertificateErrorMessage.CustomProviderSourceLoaderNullOrEmpty); + } + else if (string.IsNullOrEmpty(credentialDescription.CustomSignedAssertionProviderName)) + { + // No provider name + _logger.LogError(CertificateErrorMessage.CustomProviderNameNullOrEmpty); + } + else if (!CustomSignedAssertionCredentialSourceLoaders!.TryGetValue(credentialDescription.CustomSignedAssertionProviderName!, out ICustomSignedAssertionProvider? sourceLoader)) + { + // No source loader for provider name + _logger.LogError(CertificateErrorMessage.CustomProviderNotFound, credentialDescription.CustomSignedAssertionProviderName); + } + else + { + // Load the credentials, if there is an error, it is coming from the user's custom extension and should be logged and propagated. + 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 aa6686779..79aeca0c0 100644 --- a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.Logger.cs +++ b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.Logger.cs @@ -10,6 +10,12 @@ namespace Microsoft.Identity.Web // Log messages for DefaultCredentialsLoader public partial class DefaultCredentialsLoader { + internal const string nameMissing = "NameMissing"; + internal static string CustomSignedAssertionProviderLoadingFailureMessage(string providerName, string sourceType, string skip) + { + return $"Failed to find custom signed assertion provider {providerName} from source {sourceType}. Will it be skipped in the future ? {skip}."; + } + /// /// Logging infrastructure /// @@ -18,13 +24,25 @@ private static class Logger private static readonly Action s_credentialLoadingFailure = LoggerMessage.Define( LogLevel.Information, - new EventId( - 7, - nameof(CredentialLoadingFailure)), - "Failed to load credential {id} from source {sourceType}. Will it be skipped in the future ? {skip}."); + new EventId(7, nameof(CredentialLoadingFailure)), + "Failed to load credential {id} from source {sourceType}. Will it be skipped in the future ? {skip}." + ); public static void CredentialLoadingFailure(ILogger logger, CredentialDescription cd, Exception? ex) => s_credentialLoadingFailure(logger, cd.Id, cd.SourceType.ToString(), cd.Skip, ex); + + private static readonly Action s_customSignedAssertionProviderLoadingFailure = + LoggerMessage.Define( + LogLevel.Information, + new EventId(8, nameof(CustomSignedAssertionProviderLoadingFailure)), + CustomSignedAssertionProviderLoadingFailureMessage("{name}", "{sourceType}", "{skip}") + ); + + public static void CustomSignedAssertionProviderLoadingFailure( + ILogger logger, + CredentialDescription cd, + Exception ex + ) => s_customSignedAssertionProviderLoadingFailure(logger, cd.CustomSignedAssertionProviderName ?? nameMissing, cd.SourceType.ToString(), cd.Skip, ex); } } } diff --git a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.cs b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.cs index 6f4dea6de..7b6cade30 100644 --- a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.cs +++ b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.cs @@ -72,7 +72,11 @@ public async Task LoadCredentialsIfNeededAsync(CredentialDescription credentialD { if (credentialDescription.CachedValue == null) { - if (CredentialSourceLoaders.TryGetValue(credentialDescription.SourceType, out ICredentialSourceLoader? loader)) + if (credentialDescription.SourceType == CredentialSource.CustomSignedAssertion) + { + await ProcessCustomSignedAssertionAsync(credentialDescription, parameters); + } + else if (CredentialSourceLoaders.TryGetValue(credentialDescription.SourceType, out ICredentialSourceLoader? loader)) { try { diff --git a/src/Microsoft.Identity.Web.Certificate/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.Certificate/InternalAPI.Unshipped.txt index e69de29bb..7e4f98311 100644 --- a/src/Microsoft.Identity.Web.Certificate/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.Certificate/InternalAPI.Unshipped.txt @@ -0,0 +1,11 @@ +const Microsoft.Identity.Web.CertificateErrorMessage.CustomProviderNameAlreadyExists = "IDW10111 The custom signed assertion provider '{0}' already exists, only the the first instance of ICustomSignedAssertionProvider with this name will be used." -> string! +const Microsoft.Identity.Web.CertificateErrorMessage.CustomProviderNameNullOrEmpty = "IDW10112 The name of the custom signed assertion provider is null or empty." -> string! +const Microsoft.Identity.Web.CertificateErrorMessage.CustomProviderNotFound = "IDW10113: The custom signed assertion provider with name '{0}' was not found. Was it registered in the service collection?" -> string! +const Microsoft.Identity.Web.CertificateErrorMessage.CustomProviderSourceLoaderNullOrEmpty = "IDW10114 The dictionary of SourceLoaders for custom signed assertion providers is null or empty." -> string! +const Microsoft.Identity.Web.DefaultCredentialsLoader.nameMissing = "NameMissing" -> 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! +static Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException.ProviderNameNullOrEmpty() -> Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException! +static Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException.SourceLoadersNullOrEmpty() -> Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException! +static Microsoft.Identity.Web.DefaultCredentialsLoader.CustomSignedAssertionProviderLoadingFailureMessage(string! providerName, string! sourceType, string! skip) -> string! diff --git a/src/Microsoft.Identity.Web.Certificate/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.Certificate/PublicAPI.Unshipped.txt index 7dc5c5811..862e7185a 100644 --- a/src/Microsoft.Identity.Web.Certificate/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.Certificate/PublicAPI.Unshipped.txt @@ -1 +1,4 @@ #nullable enable +Microsoft.Identity.Web.DefaultCertificateLoader.DefaultCertificateLoader(System.Collections.Generic.IEnumerable! customSignedAssertionProviders, Microsoft.Extensions.Logging.ILogger? logger) -> void +Microsoft.Identity.Web.DefaultCredentialsLoader.CustomSignedAssertionCredentialSourceLoaders.get -> System.Collections.Generic.IDictionary? +Microsoft.Identity.Web.DefaultCredentialsLoader.DefaultCredentialsLoader(System.Collections.Generic.IEnumerable! customSignedAssertionProviders, Microsoft.Extensions.Logging.ILogger? logger) -> void diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/ConfidentialClientApplicationBuilderExtension.Logger.cs b/src/Microsoft.Identity.Web.TokenAcquisition/ConfidentialClientApplicationBuilderExtension.Logger.cs index 477143708..8352fd046 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/ConfidentialClientApplicationBuilderExtension.Logger.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/ConfidentialClientApplicationBuilderExtension.Logger.cs @@ -35,6 +35,12 @@ internal static class Logger LoggingEventId.UsingSignedAssertionFromVault, "[MsIdWeb] Using signed assertion from {signedAssertionUri} as client credentials. "); + private static readonly Action s_usingSignedAssertionFromCustomProvider = + LoggerMessage.Define( + LogLevel.Information, + LoggingEventId.UsingSignedAssertionFromCustomProvider, + "[MsIdWeb] Using signed assertion from {signedAssertionUri} as client credentials. "); + private static readonly Action s_usingCertThumbprint = LoggerMessage.Define( LogLevel.Information, @@ -49,9 +55,9 @@ internal static class Logger private static readonly Action s_credentialAttemptFailed = LoggerMessage.Define( - LogLevel.Information, - LoggingEventId.CredentialLoadAttemptFailed, - "[MsIdWeb] Loading the credential from CredentialDescription Id={Id} failed. Will the credential be re-attempted? - {Skip}."); + LogLevel.Information, + LoggingEventId.CredentialLoadAttemptFailed, + "[MsIdWeb] Loading the credential from CredentialDescription Id={Id} failed. Will the credential be re-attempted? - {Skip}."); /// /// Logger for attempting to use a CredentialDescription with MSAL @@ -131,6 +137,14 @@ public static void UsingSignedAssertionFromVault( ILogger logger, string signedAssertionUri) => s_usingSignedAssertionFromVault(logger, signedAssertionUri, default!); + /// + /// Logger for handling information specific to ConfidentialClientApplicationBuilderExtension. + /// + /// ILogger. + /// + public static void UsingSignedAssertionFromCustomProvider( + ILogger logger, + string signedAssertionUri) => s_usingSignedAssertionFromCustomProvider(logger, signedAssertionUri, default!); /// /// Logger for handling information specific to ConfidentialClientApplicationBuilderExtension. diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/ConfidentialClientApplicationBuilderExtension.cs b/src/Microsoft.Identity.Web.TokenAcquisition/ConfidentialClientApplicationBuilderExtension.cs index b95db70d4..324f0e458 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/ConfidentialClientApplicationBuilderExtension.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/ConfidentialClientApplicationBuilderExtension.cs @@ -115,6 +115,14 @@ public static async Task WithClientCredent return credential; } } + if (credential.SourceType == CredentialSource.CustomSignedAssertion) + { + if (!credential.Skip) + { + Logger.UsingSignedAssertionFromCustomProvider(logger, credential.CustomSignedAssertionProviderName ?? "undefined"); + return credential; + } + } } if (credential.CredentialType == CredentialType.Certificate) diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/LoggingEventId.cs b/src/Microsoft.Identity.Web.TokenAcquisition/LoggingEventId.cs index 746132727..2abac1a8b 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/LoggingEventId.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/LoggingEventId.cs @@ -27,6 +27,7 @@ internal static class LoggingEventId public static readonly EventId UsingSignedAssertionFromVault = new EventId(404, "UsingSignedAssertionFromVault"); public static readonly EventId CredentialLoadAttempt = new EventId(405, "CredentialLoadAttempt"); public static readonly EventId CredentialLoadAttemptFailed = new EventId(406, "CredentialLoadAttemptFailed"); + public static readonly EventId UsingSignedAssertionFromCustomProvider = new EventId(407, "UsingSignedAssertionFromCustomProvider"); #pragma warning restore IDE1006 // Naming Styles } diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/InternalAPI.Unshipped.txt index 9ee6a81d4..6cec0de3c 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/InternalAPI.Unshipped.txt @@ -6,4 +6,6 @@ Microsoft.Identity.Web.MergedOptions.PreparedInstance.set -> void Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForApp(Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions) -> void Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForTestUser(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> void readonly Microsoft.Identity.Web.TokenAcquisition.tokenAcquisitionExtensionOptionsMonitor -> Microsoft.Extensions.Options.IOptionsMonitor? +static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingSignedAssertionFromCustomProvider(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionUri) -> void static Microsoft.Identity.Web.TokenAcquisition.ResolveTenant(string? tenant, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> string? +static readonly Microsoft.Identity.Web.LoggingEventId.UsingSignedAssertionFromCustomProvider -> Microsoft.Extensions.Logging.EventId diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/InternalAPI.Unshipped.txt index 9ee6a81d4..6cec0de3c 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/InternalAPI.Unshipped.txt @@ -6,4 +6,6 @@ Microsoft.Identity.Web.MergedOptions.PreparedInstance.set -> void Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForApp(Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions) -> void Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForTestUser(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> void readonly Microsoft.Identity.Web.TokenAcquisition.tokenAcquisitionExtensionOptionsMonitor -> Microsoft.Extensions.Options.IOptionsMonitor? +static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingSignedAssertionFromCustomProvider(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionUri) -> void static Microsoft.Identity.Web.TokenAcquisition.ResolveTenant(string? tenant, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> string? +static readonly Microsoft.Identity.Web.LoggingEventId.UsingSignedAssertionFromCustomProvider -> Microsoft.Extensions.Logging.EventId diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net6.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net6.0/InternalAPI.Unshipped.txt index 9ee6a81d4..6cec0de3c 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net6.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net6.0/InternalAPI.Unshipped.txt @@ -6,4 +6,6 @@ Microsoft.Identity.Web.MergedOptions.PreparedInstance.set -> void Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForApp(Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions) -> void Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForTestUser(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> void readonly Microsoft.Identity.Web.TokenAcquisition.tokenAcquisitionExtensionOptionsMonitor -> Microsoft.Extensions.Options.IOptionsMonitor? +static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingSignedAssertionFromCustomProvider(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionUri) -> void static Microsoft.Identity.Web.TokenAcquisition.ResolveTenant(string? tenant, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> string? +static readonly Microsoft.Identity.Web.LoggingEventId.UsingSignedAssertionFromCustomProvider -> Microsoft.Extensions.Logging.EventId diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net7.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net7.0/InternalAPI.Unshipped.txt index 9ee6a81d4..6cec0de3c 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net7.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net7.0/InternalAPI.Unshipped.txt @@ -6,4 +6,6 @@ Microsoft.Identity.Web.MergedOptions.PreparedInstance.set -> void Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForApp(Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions) -> void Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForTestUser(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> void readonly Microsoft.Identity.Web.TokenAcquisition.tokenAcquisitionExtensionOptionsMonitor -> Microsoft.Extensions.Options.IOptionsMonitor? +static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingSignedAssertionFromCustomProvider(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionUri) -> void static Microsoft.Identity.Web.TokenAcquisition.ResolveTenant(string? tenant, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> string? +static readonly Microsoft.Identity.Web.LoggingEventId.UsingSignedAssertionFromCustomProvider -> Microsoft.Extensions.Logging.EventId diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Unshipped.txt index 9ee6a81d4..6cec0de3c 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Unshipped.txt @@ -6,4 +6,6 @@ Microsoft.Identity.Web.MergedOptions.PreparedInstance.set -> void Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForApp(Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions) -> void Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForTestUser(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> void readonly Microsoft.Identity.Web.TokenAcquisition.tokenAcquisitionExtensionOptionsMonitor -> Microsoft.Extensions.Options.IOptionsMonitor? +static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingSignedAssertionFromCustomProvider(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionUri) -> void static Microsoft.Identity.Web.TokenAcquisition.ResolveTenant(string? tenant, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> string? +static readonly Microsoft.Identity.Web.LoggingEventId.UsingSignedAssertionFromCustomProvider -> Microsoft.Extensions.Logging.EventId diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Unshipped.txt index 9ee6a81d4..6cec0de3c 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Unshipped.txt @@ -6,4 +6,6 @@ Microsoft.Identity.Web.MergedOptions.PreparedInstance.set -> void Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForApp(Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions) -> void Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForTestUser(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> void readonly Microsoft.Identity.Web.TokenAcquisition.tokenAcquisitionExtensionOptionsMonitor -> Microsoft.Extensions.Options.IOptionsMonitor? +static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingSignedAssertionFromCustomProvider(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionUri) -> void static Microsoft.Identity.Web.TokenAcquisition.ResolveTenant(string? tenant, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> string? +static readonly Microsoft.Identity.Web.LoggingEventId.UsingSignedAssertionFromCustomProvider -> Microsoft.Extensions.Logging.EventId diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt index 9ee6a81d4..6cec0de3c 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt @@ -6,4 +6,6 @@ Microsoft.Identity.Web.MergedOptions.PreparedInstance.set -> void Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForApp(Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions) -> void Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForTestUser(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> void readonly Microsoft.Identity.Web.TokenAcquisition.tokenAcquisitionExtensionOptionsMonitor -> Microsoft.Extensions.Options.IOptionsMonitor? +static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingSignedAssertionFromCustomProvider(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionUri) -> void static Microsoft.Identity.Web.TokenAcquisition.ResolveTenant(string? tenant, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> string? +static readonly Microsoft.Identity.Web.LoggingEventId.UsingSignedAssertionFromCustomProvider -> Microsoft.Extensions.Logging.EventId diff --git a/src/Microsoft.Identity.Web/PublicAPI/net6.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web/PublicAPI/net6.0/InternalAPI.Unshipped.txt index e69de29bb..84390f04e 100644 --- a/src/Microsoft.Identity.Web/PublicAPI/net6.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web/PublicAPI/net6.0/InternalAPI.Unshipped.txt @@ -0,0 +1,5 @@ +Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException +Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException.CustomSignedAssertionProviderNotFoundException(string! message) -> void +static Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException.ProviderNameNullOrEmpty() -> Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException! +static Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException.ProviderNameNotFound(string! name) -> Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException! +static Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException.CustomProviderSourceLoaderNullOrEmpty() -> Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException! diff --git a/src/Microsoft.Identity.Web/PublicAPI/net7.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web/PublicAPI/net7.0/InternalAPI.Unshipped.txt index e69de29bb..84390f04e 100644 --- a/src/Microsoft.Identity.Web/PublicAPI/net7.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web/PublicAPI/net7.0/InternalAPI.Unshipped.txt @@ -0,0 +1,5 @@ +Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException +Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException.CustomSignedAssertionProviderNotFoundException(string! message) -> void +static Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException.ProviderNameNullOrEmpty() -> Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException! +static Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException.ProviderNameNotFound(string! name) -> Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException! +static Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException.CustomProviderSourceLoaderNullOrEmpty() -> Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException! diff --git a/src/Microsoft.Identity.Web/PublicAPI/net8.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web/PublicAPI/net8.0/InternalAPI.Unshipped.txt index e69de29bb..84390f04e 100644 --- a/src/Microsoft.Identity.Web/PublicAPI/net8.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web/PublicAPI/net8.0/InternalAPI.Unshipped.txt @@ -0,0 +1,5 @@ +Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException +Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException.CustomSignedAssertionProviderNotFoundException(string! message) -> void +static Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException.ProviderNameNullOrEmpty() -> Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException! +static Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException.ProviderNameNotFound(string! name) -> Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException! +static Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException.CustomProviderSourceLoaderNullOrEmpty() -> Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException! diff --git a/src/Microsoft.Identity.Web/PublicAPI/net9.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web/PublicAPI/net9.0/InternalAPI.Unshipped.txt index e69de29bb..84390f04e 100644 --- a/src/Microsoft.Identity.Web/PublicAPI/net9.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web/PublicAPI/net9.0/InternalAPI.Unshipped.txt @@ -0,0 +1,5 @@ +Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException +Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException.CustomSignedAssertionProviderNotFoundException(string! message) -> void +static Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException.ProviderNameNullOrEmpty() -> Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException! +static Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException.ProviderNameNotFound(string! name) -> Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException! +static Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException.CustomProviderSourceLoaderNullOrEmpty() -> Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException! diff --git a/tests/DevApps/aspnet-mvc/OwinWebApi/Web.config b/tests/DevApps/aspnet-mvc/OwinWebApi/Web.config index a304966e0..2d99d4971 100644 --- a/tests/DevApps/aspnet-mvc/OwinWebApi/Web.config +++ b/tests/DevApps/aspnet-mvc/OwinWebApi/Web.config @@ -58,7 +58,7 @@ - + @@ -74,7 +74,7 @@ - + @@ -82,23 +82,23 @@ - + - + - + - + - + diff --git a/tests/DevApps/aspnet-mvc/OwinWebApp/Web.config b/tests/DevApps/aspnet-mvc/OwinWebApp/Web.config index 44a7980ee..3c287d045 100644 --- a/tests/DevApps/aspnet-mvc/OwinWebApp/Web.config +++ b/tests/DevApps/aspnet-mvc/OwinWebApp/Web.config @@ -59,7 +59,7 @@ - + @@ -75,7 +75,7 @@ - + @@ -83,23 +83,23 @@ - + - + - + - + - + diff --git a/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs new file mode 100644 index 000000000..158957f21 --- /dev/null +++ b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Client; +using Microsoft.Identity.Web; +using Xunit.Sdk; + + +namespace CustomSignedAssertionProviderTests +{ + public class CustomSignedAssertionProviderExtensibilityTests + { + [Fact] + public async Task UseSignedAssertionFromCustomSignedAssertionProvider() + { + // Arrange + string expectedExceptionCode = "AADSTS50027"; + TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); + tokenAcquirerFactory.Services.AddCustomSignedAssertionProvider(); + + // this is how the authentication options can be configured in code rather than + // in the appsettings file, though using the appsettings file is recommended + /* + tokenAcquirerFactory.Services.Configure(options => + { + options.Instance = "https://login.microsoftonline.com/"; + options.TenantId = "msidlab4.onmicrosoft.com"; + options.ClientId = "f6b698c0-140c-448f-8155-4aa9bf77ceba"; + options.ClientCredentials = [ new CredentialDescription() { + SourceType = CredentialSource.CustomSignedAssertion, + CustomSignedAssertionProviderName = "MyCustomExtension" + }]; + }); + */ + IServiceProvider serviceProvider = tokenAcquirerFactory.Build(); + IAuthorizationHeaderProvider authorizationHeaderProvider = serviceProvider.GetRequiredService(); + + try + { + // Act + _ = await authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync("https://graph.microsoft.com/.default"); + } + catch (MsalServiceException MsalEx) + { + // Assert + Assert.Contains(expectedExceptionCode, MsalEx.Message, StringComparison.InvariantCulture); + } + catch (Exception ex) when (ex is not XunitException) + { + Assert.Fail(ex.Message); + } + } + } +} diff --git a/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensions.cs b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensions.cs new file mode 100644 index 000000000..f23d7373a --- /dev/null +++ b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensions.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Identity.Abstractions; + +namespace CustomSignedAssertionProviderTests +{ + public static class CustomSignedAssertionProviderExtensions + { + public static IServiceCollection AddCustomSignedAssertionProvider(this IServiceCollection services) + { + services.AddSingleton(); + return services; + } + } +} diff --git a/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderTests.csproj b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderTests.csproj new file mode 100644 index 000000000..2cb4fef1d --- /dev/null +++ b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderTests.csproj @@ -0,0 +1,37 @@ + + + + net9.0 + ../../../build/MSAL.snk + enable + false + + + + + Always + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/E2E Tests/CustomSignedAssertionProviderTests/MyCustomSignedAssertionLoader.cs b/tests/E2E Tests/CustomSignedAssertionProviderTests/MyCustomSignedAssertionLoader.cs new file mode 100644 index 000000000..d75b2cb46 --- /dev/null +++ b/tests/E2E Tests/CustomSignedAssertionProviderTests/MyCustomSignedAssertionLoader.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Identity.Abstractions; + +namespace CustomSignedAssertionProviderTests +{ + internal class MyCustomSignedAssertionLoader : ICustomSignedAssertionProvider + { + private readonly ILogger _logger; + + public MyCustomSignedAssertionLoader(ILogger logger) + { + _logger = logger; + } + + public CredentialSource CredentialSource => CredentialSource.CustomSignedAssertion; + + public string Name => "MyCustomExtension"; + + public async Task LoadIfNeededAsync(CredentialDescription credentialDescription, CredentialSourceLoaderParameters? parameters = null) + { + MyCustomSignedAssertionProvider? signedAssertion = credentialDescription.CachedValue as MyCustomSignedAssertionProvider; + if (credentialDescription.CachedValue == null) + { + signedAssertion = new MyCustomSignedAssertionProvider(credentialDescription.CustomSignedAssertionProviderData); + } + + try + { + // Try to get a signed assertion, and if it fails, move to the next credentials + _ = await signedAssertion!.GetSignedAssertionAsync(null); + credentialDescription.CachedValue = signedAssertion; + } + catch (Exception ex) + { + if (_logger != null) + { + _logger.LogError(42, "Failed to get signed assertion from {ProviderName}. exception occurred: {Message}. Setting skip to true.", credentialDescription.CustomSignedAssertionProviderName, ex.Message); + } + credentialDescription.Skip = true; + throw; + } + } + } +} diff --git a/tests/E2E Tests/CustomSignedAssertionProviderTests/MyCustomSignedAssertionProvider.cs b/tests/E2E Tests/CustomSignedAssertionProviderTests/MyCustomSignedAssertionProvider.cs new file mode 100644 index 000000000..3500e6e00 --- /dev/null +++ b/tests/E2E Tests/CustomSignedAssertionProviderTests/MyCustomSignedAssertionProvider.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Identity.Client; +using Microsoft.Identity.Web; + +namespace CustomSignedAssertionProviderTests +{ + internal class MyCustomSignedAssertionProvider : ClientAssertionProviderBase + { + public MyCustomSignedAssertionProvider(Dictionary? properties) + { + // Implement logic to extract information from the properties passed in the configuration. + } + + protected override Task GetClientAssertionAsync(AssertionRequestOptions? assertionRequestOptions) + { + // Implement logic to get the signed assertion, which is probably going to be a call to a service. + // This call can be parameterized by using the parameters from the properties arg in the constructor. + + // In this sample code we just create an empty signed assertion and return it. + var clientAssertion = new ClientAssertion("FakeAssertion", DateTimeOffset.Now); + return Task.FromResult(clientAssertion); + } + } +} diff --git a/tests/E2E Tests/CustomSignedAssertionProviderTests/appsettings.json b/tests/E2E Tests/CustomSignedAssertionProviderTests/appsettings.json new file mode 100644 index 000000000..991d7b190 --- /dev/null +++ b/tests/E2E Tests/CustomSignedAssertionProviderTests/appsettings.json @@ -0,0 +1,13 @@ +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "msidlab4.onmicrosoft.com", + "ClientId": "f6b698c0-140c-448f-8155-4aa9bf77ceba", + "ClientCredentials": [ + { + "SourceType": "CustomSignedAssertion", + "CustomSignedAssertionProviderName": "MyCustomExtension" + } + ] + } +} diff --git a/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs b/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs new file mode 100644 index 000000000..7b89e2482 --- /dev/null +++ b/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs @@ -0,0 +1,229 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Identity.Abstractions; +using Xunit; + +namespace Microsoft.Identity.Web.Test +{ + public class CustomSignedAssertionProviderTests + { + [Fact] + public void Constructor_NullProviders_ThrowsArgumentNullException() + { + // Arrange + var loggerMock = new CustomMockLogger(); + // Act & Assert + Assert.Throws(() => new DefaultCredentialsLoader(null!, loggerMock)); + } + + [Theory] + [MemberData(nameof(CustomSignedAssertionProviderLoggingTestData), DisableDiscoveryEnumeration = true)] + public async Task ProcessCustomSignedAssertionAsync_Tests(CustomSignedAssertionProviderTheoryData data) + { + // Arrange + var loggerMock = new CustomMockLogger(); + + var loader = new DefaultCredentialsLoader(data.AssertionProviderList, loggerMock); + + // Act + try + { + await loader.LoadCredentialsIfNeededAsync(data.CredentialDescription, null); + } + catch (Exception ex) + { + Assert.Equal(data.ExpectedExceptionMessage, ex.Message); + + // This is validating the logging behavior defined by DefaultCredentialsLoader.Logger.CustomSignedAssertionProviderLoadingFailure + if (data.ExpectedLogMessage is not null) + { + Assert.Contains(loggerMock.LoggedMessages, log => log.LogLevel == data.ExpectedLogLevel && log.Message.Contains(data.ExpectedLogMessage, StringComparison.InvariantCulture)); + } + return; + } + + // Assert + if (data.ExpectedLogMessage is not null) + { + Assert.Contains(loggerMock.LoggedMessages, log => log.LogLevel == data.ExpectedLogLevel && log.Message.Contains(data.ExpectedLogMessage, StringComparison.InvariantCulture)); + } + else + { + Assert.DoesNotContain(loggerMock.LoggedMessages, log => log.LogLevel == data.ExpectedLogLevel); + } + } + + public static TheoryData CustomSignedAssertionProviderLoggingTestData() + { + return + [ + // No source loaders + new CustomSignedAssertionProviderTheoryData + { + AssertionProviderList = [], + CredentialDescription = new CredentialDescription + { + CustomSignedAssertionProviderName = "Provider1", + SourceType = CredentialSource.CustomSignedAssertion, + Skip = false + }, + ExpectedLogLevel = LogLevel.Error, + ExpectedLogMessage = CertificateErrorMessage.CustomProviderSourceLoaderNullOrEmpty + }, + + // No provider name given + new CustomSignedAssertionProviderTheoryData + { + AssertionProviderList = [new SuccessfulCustomSignedAssertionProvider("Provider2")], + CredentialDescription = new CredentialDescription + { + CustomSignedAssertionProviderName = null, + SourceType = CredentialSource.CustomSignedAssertion + }, + ExpectedLogLevel = LogLevel.Error, + ExpectedLogMessage = CertificateErrorMessage.CustomProviderNameNullOrEmpty + }, + + // Given provider name not found + new CustomSignedAssertionProviderTheoryData + { + AssertionProviderList = [new SuccessfulCustomSignedAssertionProvider("NotProvider3")], + CredentialDescription = new CredentialDescription + { + CustomSignedAssertionProviderName = "Provider3", + SourceType = CredentialSource.CustomSignedAssertion + }, + ExpectedLogLevel = LogLevel.Error, + ExpectedLogMessage = string.Format(CultureInfo.InvariantCulture, CertificateErrorMessage.CustomProviderNotFound, "Provider3") + }, + + // Happy path (no logging expected) + new CustomSignedAssertionProviderTheoryData + { + AssertionProviderList = [new SuccessfulCustomSignedAssertionProvider("Provider4")], + CredentialDescription = new CredentialDescription + { + CustomSignedAssertionProviderName = "Provider4", + SourceType = CredentialSource.CustomSignedAssertion + } + }, + + // CustomSignedAssertionProvider (i.e. the user's extension) throws an exception + new CustomSignedAssertionProviderTheoryData + { + AssertionProviderList = [new FailingCustomSignedAssertionProvider("Provider5")], + CredentialDescription = new CredentialDescription + { + CustomSignedAssertionProviderName = "Provider5", + SourceType = CredentialSource.CustomSignedAssertion + }, + ExpectedLogLevel = LogLevel.Information, + ExpectedLogMessage = string.Format + ( + CultureInfo.InvariantCulture, + DefaultCredentialsLoader.CustomSignedAssertionProviderLoadingFailureMessage + ( + "Provider5", + CredentialSource.CustomSignedAssertion.ToString(), + false.ToString() + ) + ), + ExpectedExceptionMessage = FailingCustomSignedAssertionProvider.ExceptionMessage + }, + + // Multiple providers with the same name + new CustomSignedAssertionProviderTheoryData + { + AssertionProviderList = [new SuccessfulCustomSignedAssertionProvider("Provider6"), new SuccessfulCustomSignedAssertionProvider("Provider6")], + CredentialDescription = new CredentialDescription + { + CustomSignedAssertionProviderName = "Provider6", + SourceType = CredentialSource.CustomSignedAssertion + }, + ExpectedLogLevel = LogLevel.Warning, + ExpectedLogMessage = string.Format(CultureInfo.InvariantCulture, CertificateErrorMessage.CustomProviderNameAlreadyExists, "Provider6") + } + ]; + } + + } + + public class CustomSignedAssertionProviderTheoryData + { + public List AssertionProviderList { get; set; } = []; + public CredentialDescription CredentialDescription { get; set; } = new CredentialDescription(); + public LogLevel ExpectedLogLevel { get; set; } + public string? ExpectedLogMessage { get; set; } + public string? ExpectedExceptionMessage { get; set; } + } + + // Custom logger implementation + sealed class CustomMockLogger : ILogger + { + public List LoggedMessages { get; } = []; + + IDisposable ILogger.BeginScope(TState state) => null!; + + bool ILogger.IsEnabled(LogLevel logLevel) => true; + + void ILogger.Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + LoggedMessages.Add(new LogEntry + { + LogLevel = logLevel, + Message = formatter(state, exception), + Exception = exception + }); + } + } + + public class LogEntry + { + public LogLevel LogLevel { get; set; } + public string Message { get; set; } = string.Empty; + public Exception? Exception { get; set; } + } + + // Helper class mocking an implementation of ICustomSignedAssertionProvider normally provided by a user where the LoadIfNeededAsync method completes without error. + internal class SuccessfulCustomSignedAssertionProvider : ICustomSignedAssertionProvider + { + public string Name { get; } + + public CredentialSource CredentialSource => CredentialSource.CustomSignedAssertion; + + public SuccessfulCustomSignedAssertionProvider(string name) + { + Name = name; + } + + public Task LoadIfNeededAsync(CredentialDescription credentialDescription, CredentialSourceLoaderParameters? parameters) + { + return Task.CompletedTask; + } + } + + // Helper class mocking an implementation of ICustomSignedAssertionProvider normally provided by a user where the LoadIfNeededAsync method throws error. + 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 :("); + } + } +}