From 8ca8810f9b74af6b26b162436ca869248e366107 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Wed, 29 Jan 2025 16:40:40 -0800 Subject: [PATCH 01/36] Added logic to DefaultCredentialsLoader to support custom signed assertion providers --- Directory.Build.props | 2 +- .../DefaultCredentialsLoader.Logger.cs | 15 +++ .../DefaultCredentialsLoader.cs | 6 +- ...tCredentialsLoaderCustomSignedAssertion.cs | 116 ++++++++++++++++++ .../InternalAPI.Unshipped.txt | 5 + .../PublicAPI.Unshipped.txt | 2 + .../net6.0/InternalAPI.Unshipped.txt | 5 + .../net7.0/InternalAPI.Unshipped.txt | 5 + .../net8.0/InternalAPI.Unshipped.txt | 5 + .../net9.0/InternalAPI.Unshipped.txt | 5 + 10 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoaderCustomSignedAssertion.cs diff --git a/Directory.Build.props b/Directory.Build.props index a9bd8beab..3fc2d18d0 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -96,7 +96,7 @@ 4.36.0 4.57.0-preview 3.1.3 - 8.0.0 + 8.1.0 8.0.5 diff --git a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.Logger.cs b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.Logger.cs index aa6686779..d6a124248 100644 --- a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.Logger.cs +++ b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.Logger.cs @@ -25,6 +25,21 @@ private static class Logger 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( + 7, + nameof(CustomSignedAssertionProviderLoadingFailure)), + "Failed to find custom signed assertion provider {name} from source {sourceType}. Will it be skipped in the future ? {skip}."); + + public static void CustomSignedAssertionProviderLoadingFailure( + ILogger logger, + CredentialDescription cd, + string providerName, + CustomSignedAssertionProviderNotFoundException ex + ) => s_customSignedAssertionProviderLoadingFailure(logger, cd.CustomSignedAssertionProviderName, 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/DefaultCredentialsLoaderCustomSignedAssertion.cs b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoaderCustomSignedAssertion.cs new file mode 100644 index 000000000..7d53272f9 --- /dev/null +++ b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoaderCustomSignedAssertion.cs @@ -0,0 +1,116 @@ +// 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.Extensions.Logging.Abstractions; +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) + { + _logger = logger ?? new NullLogger(); + + CredentialSourceLoaders = new Dictionary + { + { CredentialSource.KeyVault, new KeyVaultCertificateLoader() }, + { CredentialSource.Path, new FromPathCertificateLoader() }, + { CredentialSource.StoreWithThumbprint, new StoreWithThumbprintCertificateLoader() }, + { CredentialSource.StoreWithDistinguishedName, new StoreWithDistinguishedNameCertificateLoader() }, + { CredentialSource.Base64Encoded, new Base64EncodedCertificateLoader() }, + { CredentialSource.SignedAssertionFromManagedIdentity, new SignedAssertionFromManagedIdentityCredentialLoader(_logger) }, + { CredentialSource.SignedAssertionFilePath, new SignedAssertionFilePathCredentialsLoader(_logger) } + }; + + var CustomSignedAssertionCredentialSourceLoaders = new Dictionary(); + foreach (var provider in customSignedAssertionProviders) + { + CustomSignedAssertionCredentialSourceLoaders.Add(provider.Name ?? provider.GetType().FullName!, provider); + } + } + + /// + /// 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.CredentialLoadingFailure(_logger, credentialDescription, CustomSignedAssertionProviderNotFoundException.SourceLoadersNullOrEmpty()); + } + + // No provider name + else if (string.IsNullOrEmpty(credentialDescription.CustomSignedAssertionProviderName)) + { + Logger.CredentialLoadingFailure(_logger, credentialDescription, CustomSignedAssertionProviderNotFoundException.ProviderNameNullOrEmpty()); + } + + // No source loader for provider name + else if (!CustomSignedAssertionCredentialSourceLoaders!.TryGetValue(credentialDescription.CustomSignedAssertionProviderName!, out ICredentialSourceLoader? sourceLoader)) + { + Logger.CredentialLoadingFailure(_logger, credentialDescription, CustomSignedAssertionProviderNotFoundException.ProviderNameNotFound(credentialDescription.CustomSignedAssertionProviderName!)); + } + + // Load the credentials + else + { + await sourceLoader.LoadIfNeededAsync(credentialDescription, parameters); + } + } + } + + 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 e69de29bb..9815dc396 100644 --- a/src/Microsoft.Identity.Web.Certificate/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.Certificate/InternalAPI.Unshipped.txt @@ -0,0 +1,5 @@ +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! \ No newline at end of file diff --git a/src/Microsoft.Identity.Web.Certificate/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.Certificate/PublicAPI.Unshipped.txt index 7dc5c5811..fbeac2d5e 100644 --- a/src/Microsoft.Identity.Web.Certificate/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.Certificate/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +Microsoft.Identity.Web.DefaultCredentialsLoader.CustomSignedAssertionCredentialSourceLoaders.get -> System.Collections.Generic.IDictionary? +Microsoft.Identity.Web.DefaultCredentialsLoader.DefaultCredentialsLoader(Microsoft.Extensions.Logging.ILogger? logger, System.Collections.Generic.IEnumerable! customSignedAssertionProviders) -> void 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! From adc6ec9ecfe414d3edce70cfd25f3536385b0d5b Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Wed, 29 Jan 2025 16:47:34 -0800 Subject: [PATCH 02/36] adjusted logging message --- .../DefaultCredentialsLoader.Logger.cs | 3 +-- .../DefaultCredentialsLoaderCustomSignedAssertion.cs | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.Logger.cs b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.Logger.cs index d6a124248..acb0fb2e4 100644 --- a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.Logger.cs +++ b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.Logger.cs @@ -37,9 +37,8 @@ public static void CredentialLoadingFailure(ILogger logger, CredentialDescriptio public static void CustomSignedAssertionProviderLoadingFailure( ILogger logger, CredentialDescription cd, - string providerName, CustomSignedAssertionProviderNotFoundException ex - ) => s_customSignedAssertionProviderLoadingFailure(logger, cd.CustomSignedAssertionProviderName, cd.SourceType.ToString(), cd.Skip, 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 index 7d53272f9..83296c0f4 100644 --- a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoaderCustomSignedAssertion.cs +++ b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoaderCustomSignedAssertion.cs @@ -52,19 +52,19 @@ private async Task ProcessCustomSignedAssertionAsync(CredentialDescription crede // No source loader(s) if (CustomSignedAssertionCredentialSourceLoaders == null || !CustomSignedAssertionCredentialSourceLoaders.Any()) { - Logger.CredentialLoadingFailure(_logger, credentialDescription, CustomSignedAssertionProviderNotFoundException.SourceLoadersNullOrEmpty()); + Logger.CustomSignedAssertionProviderLoadingFailure(_logger, credentialDescription, CustomSignedAssertionProviderNotFoundException.SourceLoadersNullOrEmpty()); } // No provider name else if (string.IsNullOrEmpty(credentialDescription.CustomSignedAssertionProviderName)) { - Logger.CredentialLoadingFailure(_logger, credentialDescription, CustomSignedAssertionProviderNotFoundException.ProviderNameNullOrEmpty()); + Logger.CustomSignedAssertionProviderLoadingFailure(_logger, credentialDescription, CustomSignedAssertionProviderNotFoundException.ProviderNameNullOrEmpty()); } // No source loader for provider name else if (!CustomSignedAssertionCredentialSourceLoaders!.TryGetValue(credentialDescription.CustomSignedAssertionProviderName!, out ICredentialSourceLoader? sourceLoader)) { - Logger.CredentialLoadingFailure(_logger, credentialDescription, CustomSignedAssertionProviderNotFoundException.ProviderNameNotFound(credentialDescription.CustomSignedAssertionProviderName!)); + Logger.CustomSignedAssertionProviderLoadingFailure(_logger, credentialDescription, CustomSignedAssertionProviderNotFoundException.ProviderNameNotFound(credentialDescription.CustomSignedAssertionProviderName!)); } // Load the credentials From b82dd8b1d6eff3225f19f7b4f758e06d8a1692e5 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Wed, 29 Jan 2025 17:09:24 -0800 Subject: [PATCH 03/36] simplified constructor --- ...faultCredentialsLoaderCustomSignedAssertion.cs | 15 +-------------- .../CustomSignedAssertionProviderTests.cs | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 14 deletions(-) create mode 100644 tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs diff --git a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoaderCustomSignedAssertion.cs b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoaderCustomSignedAssertion.cs index 83296c0f4..3e0186ee8 100644 --- a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoaderCustomSignedAssertion.cs +++ b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoaderCustomSignedAssertion.cs @@ -19,21 +19,8 @@ public partial class DefaultCredentialsLoader : ICredentialsLoader /// /// /// Set of custom signed assertion providers. - public DefaultCredentialsLoader(ILogger? logger, IEnumerable customSignedAssertionProviders) + public DefaultCredentialsLoader(ILogger? logger, IEnumerable customSignedAssertionProviders) : this(logger) { - _logger = logger ?? new NullLogger(); - - CredentialSourceLoaders = new Dictionary - { - { CredentialSource.KeyVault, new KeyVaultCertificateLoader() }, - { CredentialSource.Path, new FromPathCertificateLoader() }, - { CredentialSource.StoreWithThumbprint, new StoreWithThumbprintCertificateLoader() }, - { CredentialSource.StoreWithDistinguishedName, new StoreWithDistinguishedNameCertificateLoader() }, - { CredentialSource.Base64Encoded, new Base64EncodedCertificateLoader() }, - { CredentialSource.SignedAssertionFromManagedIdentity, new SignedAssertionFromManagedIdentityCredentialLoader(_logger) }, - { CredentialSource.SignedAssertionFilePath, new SignedAssertionFilePathCredentialsLoader(_logger) } - }; - var CustomSignedAssertionCredentialSourceLoaders = new Dictionary(); foreach (var provider in customSignedAssertionProviders) { diff --git a/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs b/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs new file mode 100644 index 000000000..34663caa9 --- /dev/null +++ b/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Identity.Web.Test +{ + internal class CustomSignedAssertionProviderTests + { + } +} From 129819bfecaa64a6c96b633702f31dd0cf47bc4a Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Wed, 29 Jan 2025 19:02:17 -0800 Subject: [PATCH 04/36] Added unit tests --- ...tCredentialsLoaderCustomSignedAssertion.cs | 20 ++-- .../CustomSignedAssertionProviderTests.cs | 91 ++++++++++++++++++- 2 files changed, 102 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoaderCustomSignedAssertion.cs b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoaderCustomSignedAssertion.cs index 3e0186ee8..d4ce54f6d 100644 --- a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoaderCustomSignedAssertion.cs +++ b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoaderCustomSignedAssertion.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Identity.Abstractions; namespace Microsoft.Identity.Web @@ -21,11 +20,14 @@ public partial class DefaultCredentialsLoader : ICredentialsLoader /// Set of custom signed assertion providers. public DefaultCredentialsLoader(ILogger? logger, IEnumerable customSignedAssertionProviders) : this(logger) { - var CustomSignedAssertionCredentialSourceLoaders = new Dictionary(); + var sourceLoaderDict = new Dictionary(); + foreach (var provider in customSignedAssertionProviders) { - CustomSignedAssertionCredentialSourceLoaders.Add(provider.Name ?? provider.GetType().FullName!, provider); + sourceLoaderDict.Add(provider.Name ?? provider.GetType().FullName!, provider); } + + CustomSignedAssertionCredentialSourceLoaders = sourceLoaderDict; } /// @@ -36,29 +38,35 @@ public DefaultCredentialsLoader(ILogger? logger, IEnum private async Task ProcessCustomSignedAssertionAsync(CredentialDescription credentialDescription, CredentialSourceLoaderParameters? parameters) { + CustomSignedAssertionProviderNotFoundException providerNotFoundException; + // No source loader(s) if (CustomSignedAssertionCredentialSourceLoaders == null || !CustomSignedAssertionCredentialSourceLoaders.Any()) { - Logger.CustomSignedAssertionProviderLoadingFailure(_logger, credentialDescription, CustomSignedAssertionProviderNotFoundException.SourceLoadersNullOrEmpty()); + providerNotFoundException = CustomSignedAssertionProviderNotFoundException.SourceLoadersNullOrEmpty(); } // No provider name else if (string.IsNullOrEmpty(credentialDescription.CustomSignedAssertionProviderName)) { - Logger.CustomSignedAssertionProviderLoadingFailure(_logger, credentialDescription, CustomSignedAssertionProviderNotFoundException.ProviderNameNullOrEmpty()); + providerNotFoundException = CustomSignedAssertionProviderNotFoundException.ProviderNameNullOrEmpty(); } // No source loader for provider name else if (!CustomSignedAssertionCredentialSourceLoaders!.TryGetValue(credentialDescription.CustomSignedAssertionProviderName!, out ICredentialSourceLoader? sourceLoader)) { - Logger.CustomSignedAssertionProviderLoadingFailure(_logger, credentialDescription, CustomSignedAssertionProviderNotFoundException.ProviderNameNotFound(credentialDescription.CustomSignedAssertionProviderName!)); + providerNotFoundException = CustomSignedAssertionProviderNotFoundException.ProviderNameNotFound(credentialDescription.CustomSignedAssertionProviderName!); } // Load the credentials else { await sourceLoader.LoadIfNeededAsync(credentialDescription, parameters); + return; } + + Logger.CustomSignedAssertionProviderLoadingFailure(_logger, credentialDescription, providerNotFoundException); + throw providerNotFoundException; } } diff --git a/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs b/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs index 34663caa9..696b15601 100644 --- a/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs +++ b/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs @@ -3,13 +3,98 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Identity.Abstractions; +using Xunit; namespace Microsoft.Identity.Web.Test { - internal class CustomSignedAssertionProviderTests + public class CustomSignedAssertionProviderTests { + + [Theory] + [MemberData(nameof(CustomSignedAssertionTestData))] + public async Task ProcessCustomSignedAssertionAsync_Tests(DefaultCredentialsLoader loader, CredentialDescription credentialDescription, Exception? expectedException = null) + { + try + { + await loader.LoadCredentialsIfNeededAsync(credentialDescription, null); + } + catch (Exception ex) + { + Assert.Equal(expectedException?.Message, ex.Message); + return; + } + + Assert.Null(expectedException); + } + + 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() + }; + + // No provider name + yield return new object[] + { + new DefaultCredentialsLoader(NullLogger.Instance, new List { new CustomSignedAssertionProvider("Provider1") }), + new CredentialDescription + { + CustomSignedAssertionProviderName = null, + SourceType = CredentialSource.CustomSignedAssertion + }, + CustomSignedAssertionProviderNotFoundException.ProviderNameNullOrEmpty() + }; + + // 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") + }; + + // Happy path + yield return new object[] + { + new DefaultCredentialsLoader(NullLogger.Instance, new List { new CustomSignedAssertionProvider("Provider3") }), + new CredentialDescription + { + CustomSignedAssertionProviderName = "Provider3", + SourceType = CredentialSource.CustomSignedAssertion + } + }; + } + + } + + public class CustomSignedAssertionProvider : ICustomSignedAssertionProvider + { + public string Name { get; } + + public CredentialSource CredentialSource => CredentialSource.CustomSignedAssertion; + + public CustomSignedAssertionProvider(string name) + { + Name = name; + } + + public Task LoadIfNeededAsync(CredentialDescription credentialDescription, CredentialSourceLoaderParameters? parameters) + { + return Task.CompletedTask; + } } } From eeb9d7dad2c30bc4b5aa48918014b07c7ff47788 Mon Sep 17 00:00:00 2001 From: JLoze <103777376+JoshLozensky@users.noreply.github.com> Date: Thu, 30 Jan 2025 10:41:04 -0800 Subject: [PATCH 05/36] Update src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoaderCustomSignedAssertion.cs Co-authored-by: jennyf19 --- .../DefaultCredentialsLoaderCustomSignedAssertion.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoaderCustomSignedAssertion.cs b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoaderCustomSignedAssertion.cs index d4ce54f6d..c706cdded 100644 --- a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoaderCustomSignedAssertion.cs +++ b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoaderCustomSignedAssertion.cs @@ -35,7 +35,6 @@ public DefaultCredentialsLoader(ILogger? logger, IEnum /// public IDictionary? CustomSignedAssertionCredentialSourceLoaders { get; } - private async Task ProcessCustomSignedAssertionAsync(CredentialDescription credentialDescription, CredentialSourceLoaderParameters? parameters) { CustomSignedAssertionProviderNotFoundException providerNotFoundException; From b4096afd9b3bdf5c1a5c0f1e9c699e8754457d6d Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Thu, 30 Jan 2025 22:39:00 -0800 Subject: [PATCH 06/36] reworked error logging --- .../CertificateErrorMessage.cs | 3 + ...CredentialsLoader.CustomSignedAssertion.cs | 75 +++++++++ .../DefaultCredentialsLoader.Logger.cs | 2 +- ...tCredentialsLoaderCustomSignedAssertion.cs | 110 ------------- .../InternalAPI.Unshipped.txt | 3 + tests/Directory.Build.props | 1 + .../CustomSignedAssertionProviderTests.cs | 146 ++++++++++++++---- .../Microsoft.Identity.Web.Test.csproj | 1 + 8 files changed, 196 insertions(+), 145 deletions(-) create mode 100644 src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs delete mode 100644 src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoaderCustomSignedAssertion.cs 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 @@ + From c9ffdeff29e2a88574ca65b0fb353e303cb87a6c Mon Sep 17 00:00:00 2001 From: JLoze <103777376+JoshLozensky@users.noreply.github.com> Date: Mon, 3 Feb 2025 16:07:23 -0800 Subject: [PATCH 07/36] Update error string Co-authored-by: Jean-Marc Prieur --- .../CertificateErrorMessage.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Identity.Web.Certificate/CertificateErrorMessage.cs b/src/Microsoft.Identity.Web.Certificate/CertificateErrorMessage.cs index b70aca88f..433d978e4 100644 --- a/src/Microsoft.Identity.Web.Certificate/CertificateErrorMessage.cs +++ b/src/Microsoft.Identity.Web.Certificate/CertificateErrorMessage.cs @@ -17,7 +17,7 @@ internal static class CertificateErrorMessage "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 CustomProviderNotFound = "IDW10112: The custom signed assertion provider with name '{0}' was not found. Was it registered in the service collection?"; public const string CustomProviderSourceLoaderNullOrEmpty = "IDW10113 The dictionary of SourceLoaders for custom signed assertion providers is null or empty."; // Encoding IDW10600 = "IDW10600:" From 6cd4fdc1926652d663c817a88c330199611aacd7 Mon Sep 17 00:00:00 2001 From: JLoze <103777376+JoshLozensky@users.noreply.github.com> Date: Mon, 3 Feb 2025 16:07:41 -0800 Subject: [PATCH 08/36] extra line Co-authored-by: jennyf19 --- .../DefaultCredentialsLoader.CustomSignedAssertion.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs index b2d709ac8..2daa64637 100644 --- a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs +++ b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs @@ -8,7 +8,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Identity.Abstractions; - namespace Microsoft.Identity.Web { public partial class DefaultCredentialsLoader From f7dfa871e907750c3bd8fbc65a0c5731b2aefb06 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Mon, 3 Feb 2025 16:31:59 -0800 Subject: [PATCH 09/36] fixing public API and addressing PR comments --- ...CredentialsLoader.CustomSignedAssertion.cs | 4 ++-- .../InternalAPI.Unshipped.txt | 2 +- .../PublicAPI.Unshipped.txt | 2 +- .../CustomSignedAssertionProviderTests.cs | 19 +++++++++---------- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs index 2daa64637..88fbd85f5 100644 --- a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs +++ b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs @@ -15,9 +15,9 @@ 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) + /// + public DefaultCredentialsLoader(IEnumerable customSignedAssertionProviders, ILogger? logger) : this(logger) { var sourceLoaderDict = new Dictionary(); diff --git a/src/Microsoft.Identity.Web.Certificate/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.Certificate/InternalAPI.Unshipped.txt index 8ad8d88d1..192b686c2 100644 --- a/src/Microsoft.Identity.Web.Certificate/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.Certificate/InternalAPI.Unshipped.txt @@ -1,5 +1,5 @@ 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.CustomProviderNotFound = "IDW10112: 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 = "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 diff --git a/src/Microsoft.Identity.Web.Certificate/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.Certificate/PublicAPI.Unshipped.txt index fbeac2d5e..9d8bf39b6 100644 --- a/src/Microsoft.Identity.Web.Certificate/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.Certificate/PublicAPI.Unshipped.txt @@ -1,3 +1,3 @@ #nullable enable Microsoft.Identity.Web.DefaultCredentialsLoader.CustomSignedAssertionCredentialSourceLoaders.get -> System.Collections.Generic.IDictionary? -Microsoft.Identity.Web.DefaultCredentialsLoader.DefaultCredentialsLoader(Microsoft.Extensions.Logging.ILogger? logger, System.Collections.Generic.IEnumerable! customSignedAssertionProviders) -> void +Microsoft.Identity.Web.DefaultCredentialsLoader.DefaultCredentialsLoader(System.Collections.Generic.IEnumerable! customSignedAssertionProviders, Microsoft.Extensions.Logging.ILogger? logger) -> void diff --git a/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs b/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs index 3ed503e57..05829f4fe 100644 --- a/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs +++ b/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs @@ -26,7 +26,7 @@ public async Task ProcessCustomSignedAssertionAsync_Tests( var loggerMock = new Mock>(); loggerMock.Setup(x => x.IsEnabled(It.IsAny())).Returns(true); - var loader = new DefaultCredentialsLoader(loggerMock.Object, providerList); + var loader = new DefaultCredentialsLoader(providerList, loggerMock.Object); // Act try @@ -39,15 +39,14 @@ public async Task ProcessCustomSignedAssertionAsync_Tests( 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);*/ + loggerMock.Verify( + x => + x.Log( + It.IsAny(), + It.IsAny(), + It.Is((v, t) => v.ToString()!.Contains(expectedMessage!)), + It.IsAny(), + It.Is>((v,t) => true))); return; } From 4be7c48d9ff053eac7a7600e3f3d7ed8454c890a Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Mon, 3 Feb 2025 16:49:24 -0800 Subject: [PATCH 10/36] changed CustomSignedAssertionCredentialSourceLoader dict to use ICustomSignedAssertionProvider --- .../DefaultCredentialsLoader.CustomSignedAssertion.cs | 6 +++--- .../PublicAPI.Unshipped.txt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs index 88fbd85f5..7c86bcbd7 100644 --- a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs +++ b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs @@ -19,9 +19,9 @@ public partial class DefaultCredentialsLoader /// public DefaultCredentialsLoader(IEnumerable customSignedAssertionProviders, ILogger? logger) : this(logger) { - var sourceLoaderDict = new Dictionary(); + var sourceLoaderDict = new Dictionary(); - foreach (var provider in customSignedAssertionProviders) + foreach (ICustomSignedAssertionProvider provider in customSignedAssertionProviders) { sourceLoaderDict.Add(provider.Name ?? provider.GetType().FullName!, provider); } @@ -32,7 +32,7 @@ public DefaultCredentialsLoader(IEnumerable cust /// /// Dictionary of custom signed assertion credential source loaders, by name (fully qualified type name). /// - public IDictionary? CustomSignedAssertionCredentialSourceLoaders { get; } + public IDictionary? CustomSignedAssertionCredentialSourceLoaders { get; } private async Task ProcessCustomSignedAssertionAsync(CredentialDescription credentialDescription, CredentialSourceLoaderParameters? parameters) diff --git a/src/Microsoft.Identity.Web.Certificate/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.Certificate/PublicAPI.Unshipped.txt index 9d8bf39b6..477b39e65 100644 --- a/src/Microsoft.Identity.Web.Certificate/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.Certificate/PublicAPI.Unshipped.txt @@ -1,3 +1,3 @@ #nullable enable -Microsoft.Identity.Web.DefaultCredentialsLoader.CustomSignedAssertionCredentialSourceLoaders.get -> System.Collections.Generic.IDictionary? +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 From 39fdc6990bdb2803fb59b48667b604e939dc261e Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Mon, 3 Feb 2025 17:58:36 -0800 Subject: [PATCH 11/36] finished unit test for behavior when user extension throws an error --- ...CredentialsLoader.CustomSignedAssertion.cs | 2 +- .../CustomSignedAssertionProviderTests.cs | 44 ++++++++++--------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs index 7c86bcbd7..81249fd6f 100644 --- a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs +++ b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs @@ -50,7 +50,7 @@ private async Task ProcessCustomSignedAssertionAsync(CredentialDescription crede } // No source loader for provider name - else if (!CustomSignedAssertionCredentialSourceLoaders!.TryGetValue(credentialDescription.CustomSignedAssertionProviderName!, out ICredentialSourceLoader? sourceLoader)) + else if (!CustomSignedAssertionCredentialSourceLoaders!.TryGetValue(credentialDescription.CustomSignedAssertionProviderName!, out ICustomSignedAssertionProvider? sourceLoader)) { _logger.LogError(CertificateErrorMessage.CustomProviderNotFound, credentialDescription.CustomSignedAssertionProviderName); } diff --git a/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs b/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs index 05829f4fe..29ff266f6 100644 --- a/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs +++ b/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs @@ -5,9 +5,9 @@ using System.Collections.Generic; using System.Globalization; using System.Threading.Tasks; -using Moq; using Microsoft.Extensions.Logging; using Microsoft.Identity.Abstractions; +using Moq; using Xunit; namespace Microsoft.Identity.Web.Test @@ -38,15 +38,15 @@ public async Task ProcessCustomSignedAssertionAsync_Tests( { 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() + // This is validating the logging behavior defined by DefaultCredentialsLoader.Logger.CustomSignedAssertionProviderLoadingFailure loggerMock.Verify( - x => - x.Log( - It.IsAny(), + x => x.Log( + LogLevel.Information, It.IsAny(), - It.Is((v, t) => v.ToString()!.Contains(expectedMessage!)), + It.Is((v, t) => true), // In Microsoft.Logging.Abstractions this is a private struct which is why it is defined so loosely. It.IsAny(), - It.Is>((v,t) => true))); + It.IsAny>()), + Times.Once); return; } @@ -54,24 +54,26 @@ public async Task ProcessCustomSignedAssertionAsync_Tests( 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); + 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); + x => x.Log( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>()), + Times.Never + ); } } From fb145fd97950d5d840c8f01c2de8d7e6941018a2 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Mon, 3 Feb 2025 18:19:05 -0800 Subject: [PATCH 12/36] added to method summary --- .../DefaultCredentialsLoader.CustomSignedAssertion.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs index 81249fd6f..bd5922ce0 100644 --- a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs +++ b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs @@ -31,6 +31,7 @@ public DefaultCredentialsLoader(IEnumerable cust /// /// Dictionary of custom signed assertion credential source loaders, by name (fully qualified type name). + /// The application can add more to process additional credential sources. /// public IDictionary? CustomSignedAssertionCredentialSourceLoaders { get; } From 51681b4aec91f19758493305081c5962fa717cb1 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Mon, 3 Feb 2025 19:08:54 -0800 Subject: [PATCH 13/36] Changed to concurrent dict and added logging for duplicate keys --- .../CertificateErrorMessage.cs | 18 ++++++++++-------- ...tCredentialsLoader.CustomSignedAssertion.cs | 8 ++++++-- .../InternalAPI.Unshipped.txt | 9 +++++---- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/Microsoft.Identity.Web.Certificate/CertificateErrorMessage.cs b/src/Microsoft.Identity.Web.Certificate/CertificateErrorMessage.cs index 433d978e4..873db7739 100644 --- a/src/Microsoft.Identity.Web.Certificate/CertificateErrorMessage.cs +++ b/src/Microsoft.Identity.Web.Certificate/CertificateErrorMessage.cs @@ -10,15 +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 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. Was it registered in the service collection?"; - public const string CustomProviderSourceLoaderNullOrEmpty = "IDW10113 The dictionary of SourceLoaders for custom signed assertion providers is null or empty."; + public const string CustomProviderNameAlreadyExists = + "IDW10111 The custom signed assertion provider '{ProviderName}' already exists, only the the first instance of ICustomSignedAssertionProvider in the provided IEnumerable 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 '{ProviderName}' 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/DefaultCredentialsLoader.CustomSignedAssertion.cs b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs index bd5922ce0..b03d3034c 100644 --- a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs +++ b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -19,11 +20,14 @@ public partial class DefaultCredentialsLoader /// public DefaultCredentialsLoader(IEnumerable customSignedAssertionProviders, ILogger? logger) : this(logger) { - var sourceLoaderDict = new Dictionary(); + var sourceLoaderDict = new ConcurrentDictionary(); foreach (ICustomSignedAssertionProvider provider in customSignedAssertionProviders) { - sourceLoaderDict.Add(provider.Name ?? provider.GetType().FullName!, provider); + if (sourceLoaderDict.TryAdd(provider.Name ?? provider.GetType().FullName!, provider)) + { + _logger.LogWarning(CertificateErrorMessage.CustomProviderNameAlreadyExists, provider.Name ?? provider.GetType().FullName!); + } } CustomSignedAssertionCredentialSourceLoaders = sourceLoaderDict; diff --git a/src/Microsoft.Identity.Web.Certificate/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.Certificate/InternalAPI.Unshipped.txt index 192b686c2..507cd8406 100644 --- a/src/Microsoft.Identity.Web.Certificate/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.Certificate/InternalAPI.Unshipped.txt @@ -1,8 +1,9 @@ -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. Was it registered in the service collection?" -> string! -const Microsoft.Identity.Web.CertificateErrorMessage.CustomProviderSourceLoaderNullOrEmpty = "IDW10113 The dictionary of SourceLoaders for custom signed assertion providers is null or empty." -> string! +const Microsoft.Identity.Web.CertificateErrorMessage.CustomProviderNameAlreadyExists = "IDW10111 The custom signed assertion provider '{ProviderName}' already exists, only the the first instance of ICustomSignedAssertionProvider in the provided IEnumerable 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 '{ProviderName}' 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! 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! \ No newline at end of file +static Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException.SourceLoadersNullOrEmpty() -> Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException! From 79fc329a49aa8fd6d623954c475b65b30db6c637 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Mon, 3 Feb 2025 21:58:38 -0800 Subject: [PATCH 14/36] Added more specificity to tests and also added a check for duplicate keys --- .../CertificateErrorMessage.cs | 4 +-- ...CredentialsLoader.CustomSignedAssertion.cs | 15 ++++++---- .../InternalAPI.Unshipped.txt | 4 +-- .../CustomSignedAssertionProviderTests.cs | 29 +++++++++++++++---- 4 files changed, 37 insertions(+), 15 deletions(-) diff --git a/src/Microsoft.Identity.Web.Certificate/CertificateErrorMessage.cs b/src/Microsoft.Identity.Web.Certificate/CertificateErrorMessage.cs index 873db7739..f2e370029 100644 --- a/src/Microsoft.Identity.Web.Certificate/CertificateErrorMessage.cs +++ b/src/Microsoft.Identity.Web.Certificate/CertificateErrorMessage.cs @@ -17,9 +17,9 @@ internal static class CertificateErrorMessage "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 '{ProviderName}' already exists, only the the first instance of ICustomSignedAssertionProvider in the provided IEnumerable will be used."; + "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 '{ProviderName}' was not found. Was it registered in the service collection?"; + 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:" diff --git a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs index b03d3034c..533133cc9 100644 --- a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs +++ b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs @@ -2,9 +2,7 @@ // Licensed under the MIT License. using System; -using System.Collections.Concurrent; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Identity.Abstractions; @@ -20,13 +18,18 @@ public partial class DefaultCredentialsLoader /// public DefaultCredentialsLoader(IEnumerable customSignedAssertionProviders, ILogger? logger) : this(logger) { - var sourceLoaderDict = new ConcurrentDictionary(); + var sourceLoaderDict = new Dictionary(); foreach (ICustomSignedAssertionProvider provider in customSignedAssertionProviders) { - if (sourceLoaderDict.TryAdd(provider.Name ?? provider.GetType().FullName!, provider)) + string providerName = provider.Name ?? provider.GetType().FullName!; + if (sourceLoaderDict.ContainsKey(providerName)) { - _logger.LogWarning(CertificateErrorMessage.CustomProviderNameAlreadyExists, provider.Name ?? provider.GetType().FullName!); + _logger.LogWarning(CertificateErrorMessage.CustomProviderNameAlreadyExists, providerName); + } + else + { + sourceLoaderDict.Add(providerName, provider); } } @@ -43,7 +46,7 @@ public DefaultCredentialsLoader(IEnumerable cust private async Task ProcessCustomSignedAssertionAsync(CredentialDescription credentialDescription, CredentialSourceLoaderParameters? parameters) { // No source loader(s) - if (CustomSignedAssertionCredentialSourceLoaders == null || !CustomSignedAssertionCredentialSourceLoaders.Any()) + if (CustomSignedAssertionCredentialSourceLoaders == null || CustomSignedAssertionCredentialSourceLoaders.Count == 0) { _logger.LogError(CertificateErrorMessage.CustomProviderSourceLoaderNullOrEmpty); } diff --git a/src/Microsoft.Identity.Web.Certificate/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.Certificate/InternalAPI.Unshipped.txt index 507cd8406..eeb67053b 100644 --- a/src/Microsoft.Identity.Web.Certificate/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.Certificate/InternalAPI.Unshipped.txt @@ -1,6 +1,6 @@ -const Microsoft.Identity.Web.CertificateErrorMessage.CustomProviderNameAlreadyExists = "IDW10111 The custom signed assertion provider '{ProviderName}' already exists, only the the first instance of ICustomSignedAssertionProvider in the provided IEnumerable will be used." -> string! +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 '{ProviderName}' was not found. Was it registered in the service collection?" -> 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! Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException.CustomSignedAssertionProviderNotFoundException(string! message) -> void diff --git a/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs b/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs index 29ff266f6..602744d31 100644 --- a/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs +++ b/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs @@ -15,10 +15,11 @@ namespace Microsoft.Identity.Web.Test public class CustomSignedAssertionProviderTests { [Theory] - [MemberData(nameof(CustomSignedAssertionTestData))] + [MemberData(nameof(CustomSignedAssertionLoggingTestData))] public async Task ProcessCustomSignedAssertionAsync_Tests( List providerList, CredentialDescription credentialDescription, + LogLevel expectedLogLevel = LogLevel.None, string? expectedMessage = null) { // Arrange @@ -41,7 +42,7 @@ public async Task ProcessCustomSignedAssertionAsync_Tests( // This is validating the logging behavior defined by DefaultCredentialsLoader.Logger.CustomSignedAssertionProviderLoadingFailure loggerMock.Verify( x => x.Log( - LogLevel.Information, + expectedLogLevel, It.IsAny(), It.Is((v, t) => true), // In Microsoft.Logging.Abstractions this is a private struct which is why it is defined so loosely. It.IsAny(), @@ -55,7 +56,7 @@ public async Task ProcessCustomSignedAssertionAsync_Tests( { loggerMock.Verify( x => x.Log( - LogLevel.Error, + expectedLogLevel, It.IsAny(), It.Is((v, t) => v.ToString()!.Contains(expectedMessage)), It.IsAny(), @@ -77,7 +78,7 @@ public async Task ProcessCustomSignedAssertionAsync_Tests( } } - public static IEnumerable CustomSignedAssertionTestData() + public static IEnumerable CustomSignedAssertionLoggingTestData() { // No source loaders yield return new object[] @@ -89,6 +90,7 @@ public static IEnumerable CustomSignedAssertionTestData() SourceType = CredentialSource.CustomSignedAssertion, Skip = false }, + LogLevel.Error, CertificateErrorMessage.CustomProviderSourceLoaderNullOrEmpty }; @@ -101,6 +103,7 @@ public static IEnumerable CustomSignedAssertionTestData() CustomSignedAssertionProviderName = null, SourceType = CredentialSource.CustomSignedAssertion }, + LogLevel.Error, CertificateErrorMessage.CustomProviderNameNullOrEmpty }; @@ -113,6 +116,7 @@ public static IEnumerable CustomSignedAssertionTestData() CustomSignedAssertionProviderName = "Provider3", SourceType = CredentialSource.CustomSignedAssertion }, + LogLevel.Error, string.Format(CultureInfo.InvariantCulture, CertificateErrorMessage.CustomProviderNotFound, "Provider3") }; @@ -136,12 +140,26 @@ public static IEnumerable CustomSignedAssertionTestData() CustomSignedAssertionProviderName = "Provider5", SourceType = CredentialSource.CustomSignedAssertion }, + LogLevel.Information, FailingCustomSignedAssertionProvider.ExceptionMessage }; + + // Multiple providers with the same name + yield return new object[] + { + new List { new SuccessfulCustomSignedAssertionProvider("Provider6"), new SuccessfulCustomSignedAssertionProvider("Provider6") }, + new CredentialDescription + { + CustomSignedAssertionProviderName = "Provider6", + SourceType = CredentialSource.CustomSignedAssertion + }, + LogLevel.Warning, + string.Format(CultureInfo.InvariantCulture, CertificateErrorMessage.CustomProviderNameAlreadyExists, "Provider6") + }; } } - // Helper class + // 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; } @@ -159,6 +177,7 @@ public Task LoadIfNeededAsync(CredentialDescription credentialDescription, Crede } } + // 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; } From 8520c07a7ea6321cd7a14ec889e1c487e68d92b1 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Tue, 4 Feb 2025 16:38:42 -0800 Subject: [PATCH 15/36] Added null check and test --- .../DefaultCredentialsLoader.CustomSignedAssertion.cs | 9 +++++++++ .../CustomSignedAssertionProviderTests.cs | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs index 533133cc9..8757c3afa 100644 --- a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs +++ b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Identity.Abstractions; @@ -18,6 +19,14 @@ public partial class DefaultCredentialsLoader /// public DefaultCredentialsLoader(IEnumerable customSignedAssertionProviders, ILogger? logger) : this(logger) { +#if NET7_0_OR_GREATER + ArgumentNullException.ThrowIfNull(customSignedAssertionProviders, nameof(customSignedAssertionProviders)); +# else + if (customSignedAssertionProviders is null) + { + throw new ArgumentNullException(nameof(customSignedAssertionProviders)); + } +#endif var sourceLoaderDict = new Dictionary(); foreach (ICustomSignedAssertionProvider provider in customSignedAssertionProviders) diff --git a/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs b/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs index 602744d31..056443db8 100644 --- a/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs +++ b/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs @@ -14,6 +14,15 @@ namespace Microsoft.Identity.Web.Test { public class CustomSignedAssertionProviderTests { + [Fact] + public void Constructor_NullProviders_ThrowsArgumentNullException() + { + // Arrange + var loggerMock = new CustomLogger(); + // Act & Assert + Assert.Throws(() => new DefaultCredentialsLoader(null!, loggerMock)); + } + [Theory] [MemberData(nameof(CustomSignedAssertionLoggingTestData))] public async Task ProcessCustomSignedAssertionAsync_Tests( From c6ca484c20396f611e16e52bd4eb2e765e09914c Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Tue, 4 Feb 2025 16:42:07 -0800 Subject: [PATCH 16/36] Added custom mock logger to unit tests --- .../DefaultCredentialsLoader.Logger.cs | 22 ++- .../InternalAPI.Unshipped.txt | 2 + .../CustomSignedAssertionProviderTests.cs | 176 ++++++++++-------- 3 files changed, 111 insertions(+), 89 deletions(-) diff --git a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.Logger.cs b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.Logger.cs index 3bbe7d35a..afeb05dd6 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,10 +24,9 @@ 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); @@ -29,16 +34,15 @@ public static void CredentialLoadingFailure(ILogger logger, CredentialDescriptio private static readonly Action s_customSignedAssertionProviderLoadingFailure = LoggerMessage.Define( LogLevel.Information, - new EventId( - 7, - nameof(CustomSignedAssertionProviderLoadingFailure)), - "Failed to find custom signed assertion provider {name} from source {sourceType}. Will it be skipped in the future ? {skip}."); + new EventId(7, 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); + ) => s_customSignedAssertionProviderLoadingFailure(logger, cd.CustomSignedAssertionProviderName ?? nameMissing, cd.SourceType.ToString(), cd.Skip, ex); } } } diff --git a/src/Microsoft.Identity.Web.Certificate/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.Certificate/InternalAPI.Unshipped.txt index eeb67053b..7e4f98311 100644 --- a/src/Microsoft.Identity.Web.Certificate/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.Certificate/InternalAPI.Unshipped.txt @@ -2,8 +2,10 @@ const Microsoft.Identity.Web.CertificateErrorMessage.CustomProviderNameAlreadyEx 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/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs b/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs index 056443db8..cf080f967 100644 --- a/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs +++ b/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs @@ -7,7 +7,6 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Identity.Abstractions; -using Moq; using Xunit; namespace Microsoft.Identity.Web.Test @@ -18,7 +17,7 @@ public class CustomSignedAssertionProviderTests public void Constructor_NullProviders_ThrowsArgumentNullException() { // Arrange - var loggerMock = new CustomLogger(); + var loggerMock = new CustomMockLogger(); // Act & Assert Assert.Throws(() => new DefaultCredentialsLoader(null!, loggerMock)); } @@ -29,61 +28,39 @@ public async Task ProcessCustomSignedAssertionAsync_Tests( List providerList, CredentialDescription credentialDescription, LogLevel expectedLogLevel = LogLevel.None, - string? expectedMessage = null) + string? expectedLogMessage = null, + string? expectedExceptionMessage = null) { // Arrange - var loggedMessages = new List(); - var loggerMock = new Mock>(); - loggerMock.Setup(x => x.IsEnabled(It.IsAny())).Returns(true); + var loggerMock = new CustomMockLogger(); - var loader = new DefaultCredentialsLoader(providerList, loggerMock.Object); + var loader = new DefaultCredentialsLoader(providerList, loggerMock); // Act try { await loader.LoadCredentialsIfNeededAsync(credentialDescription, null); - } catch (Exception ex) { - Assert.Equal(expectedMessage, ex.Message); + Assert.Equal(expectedExceptionMessage, ex.Message); // This is validating the logging behavior defined by DefaultCredentialsLoader.Logger.CustomSignedAssertionProviderLoadingFailure - loggerMock.Verify( - x => x.Log( - expectedLogLevel, - It.IsAny(), - It.Is((v, t) => true), // In Microsoft.Logging.Abstractions this is a private struct which is why it is defined so loosely. - It.IsAny(), - It.IsAny>()), - Times.Once); + if (expectedLogMessage is not null) + { + Assert.Contains(loggerMock.LoggedMessages, log => log.LogLevel == expectedLogLevel && log.Message.Contains(expectedLogMessage, StringComparison.InvariantCulture)); + } return; } // Assert - if (expectedMessage != null) + if (expectedLogMessage != null) { - loggerMock.Verify( - x => x.Log( - expectedLogLevel, - It.IsAny(), - It.Is((v, t) => v.ToString()!.Contains(expectedMessage)), - It.IsAny(), - It.IsAny>()), - Times.Once - ); + Assert.Contains(loggerMock.LoggedMessages, log => log.LogLevel == expectedLogLevel && log.Message.Contains(expectedLogMessage, StringComparison.InvariantCulture)); } else { - loggerMock.Verify( - x => x.Log( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny>()), - Times.Never - ); + Assert.DoesNotContain(loggerMock.LoggedMessages, log => log.LogLevel == expectedLogLevel); } } @@ -92,82 +69,121 @@ public static IEnumerable CustomSignedAssertionLoggingTestData() // No source loaders yield return new object[] { - new List(), - new CredentialDescription - { - CustomSignedAssertionProviderName = "Provider1", - SourceType = CredentialSource.CustomSignedAssertion, - Skip = false - }, - LogLevel.Error, - CertificateErrorMessage.CustomProviderSourceLoaderNullOrEmpty + new List(), + new CredentialDescription + { + CustomSignedAssertionProviderName = "Provider1", + SourceType = CredentialSource.CustomSignedAssertion, + Skip = false + }, + LogLevel.Error, + CertificateErrorMessage.CustomProviderSourceLoaderNullOrEmpty }; // No provider name given yield return new object[] { - new List { new SuccessfulCustomSignedAssertionProvider("Provider2") }, - new CredentialDescription - { - CustomSignedAssertionProviderName = null, - SourceType = CredentialSource.CustomSignedAssertion - }, - LogLevel.Error, - CertificateErrorMessage.CustomProviderNameNullOrEmpty + new List { new SuccessfulCustomSignedAssertionProvider("Provider2") }, + new CredentialDescription + { + CustomSignedAssertionProviderName = null, + SourceType = CredentialSource.CustomSignedAssertion + }, + LogLevel.Error, + CertificateErrorMessage.CustomProviderNameNullOrEmpty }; // Given provider name not found yield return new object[] { - new List { new SuccessfulCustomSignedAssertionProvider("NotProvider3") }, - new CredentialDescription - { - CustomSignedAssertionProviderName = "Provider3", - SourceType = CredentialSource.CustomSignedAssertion - }, - LogLevel.Error, - string.Format(CultureInfo.InvariantCulture, CertificateErrorMessage.CustomProviderNotFound, "Provider3") + new List { new SuccessfulCustomSignedAssertionProvider("NotProvider3") }, + new CredentialDescription + { + CustomSignedAssertionProviderName = "Provider3", + SourceType = CredentialSource.CustomSignedAssertion + }, + LogLevel.Error, + string.Format(CultureInfo.InvariantCulture, CertificateErrorMessage.CustomProviderNotFound, "Provider3") }; // Happy path (no logging expected) yield return new object[] { - new List { new SuccessfulCustomSignedAssertionProvider("Provider4") }, - new CredentialDescription - { - CustomSignedAssertionProviderName = "Provider4", - SourceType = CredentialSource.CustomSignedAssertion - } + 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 + CredentialDescription providerFiveCredDesc = new() { CustomSignedAssertionProviderName = "Provider5", SourceType = CredentialSource.CustomSignedAssertion - }, + }; + + yield return new object[] + { + new List { new FailingCustomSignedAssertionProvider("Provider5") }, + providerFiveCredDesc, LogLevel.Information, + string.Format + ( + CultureInfo.InvariantCulture, + DefaultCredentialsLoader.CustomSignedAssertionProviderLoadingFailureMessage + ( + providerFiveCredDesc.CustomSignedAssertionProviderName ?? DefaultCredentialsLoader.nameMissing, + providerFiveCredDesc.SourceType.ToString(), + providerFiveCredDesc.Skip.ToString() + ) + ), FailingCustomSignedAssertionProvider.ExceptionMessage }; // Multiple providers with the same name yield return new object[] { - new List { new SuccessfulCustomSignedAssertionProvider("Provider6"), new SuccessfulCustomSignedAssertionProvider("Provider6") }, - new CredentialDescription - { - CustomSignedAssertionProviderName = "Provider6", - SourceType = CredentialSource.CustomSignedAssertion - }, - LogLevel.Warning, - string.Format(CultureInfo.InvariantCulture, CertificateErrorMessage.CustomProviderNameAlreadyExists, "Provider6") + new List { new SuccessfulCustomSignedAssertionProvider("Provider6"), new SuccessfulCustomSignedAssertionProvider("Provider6") }, + new CredentialDescription + { + CustomSignedAssertionProviderName = "Provider6", + SourceType = CredentialSource.CustomSignedAssertion + }, + LogLevel.Warning, + string.Format(CultureInfo.InvariantCulture, CertificateErrorMessage.CustomProviderNameAlreadyExists, "Provider6") }; } } + // Custom logger implementation + sealed class CustomMockLogger : ILogger + { + public List LoggedMessages { get; } = new List(); + + 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 { From cb9affeb14bdd9833a3461cfa637176b9227316c Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Tue, 4 Feb 2025 16:42:49 -0800 Subject: [PATCH 17/36] changed CustomSignedAssertionCredentialSourceLoaders to protected --- .../DefaultCredentialsLoader.CustomSignedAssertion.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs index 8757c3afa..81379cc06 100644 --- a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs +++ b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs @@ -49,7 +49,7 @@ public DefaultCredentialsLoader(IEnumerable cust /// Dictionary of custom signed assertion credential source loaders, by name (fully qualified type name). /// The application can add more to process additional credential sources. /// - public IDictionary? CustomSignedAssertionCredentialSourceLoaders { get; } + protected IDictionary? CustomSignedAssertionCredentialSourceLoaders { get; } private async Task ProcessCustomSignedAssertionAsync(CredentialDescription credentialDescription, CredentialSourceLoaderParameters? parameters) From b8edcb6496a0a61a3ce1c29b77c1f3cdf6ffffa1 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Tue, 4 Feb 2025 16:59:28 -0800 Subject: [PATCH 18/36] improve null check --- .../DefaultCredentialsLoader.CustomSignedAssertion.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs index 81379cc06..6fe647a44 100644 --- a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs +++ b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs @@ -19,14 +19,7 @@ public partial class DefaultCredentialsLoader /// public DefaultCredentialsLoader(IEnumerable customSignedAssertionProviders, ILogger? logger) : this(logger) { -#if NET7_0_OR_GREATER - ArgumentNullException.ThrowIfNull(customSignedAssertionProviders, nameof(customSignedAssertionProviders)); -# else - if (customSignedAssertionProviders is null) - { - throw new ArgumentNullException(nameof(customSignedAssertionProviders)); - } -#endif + _ = Throws.IfNull(customSignedAssertionProviders); var sourceLoaderDict = new Dictionary(); foreach (ICustomSignedAssertionProvider provider in customSignedAssertionProviders) From 2a025840c677aafd86468a18b0da4839dd7603b8 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Tue, 4 Feb 2025 17:13:46 -0800 Subject: [PATCH 19/36] removed Moq dependency --- tests/Directory.Build.props | 1 - .../Microsoft.Identity.Web.Test.csproj | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 92f8696cb..5e0813a88 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -34,7 +34,6 @@ 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/Microsoft.Identity.Web.Test.csproj b/tests/Microsoft.Identity.Web.Test/Microsoft.Identity.Web.Test.csproj index f11f922a3..4b349e814 100644 --- a/tests/Microsoft.Identity.Web.Test/Microsoft.Identity.Web.Test.csproj +++ b/tests/Microsoft.Identity.Web.Test/Microsoft.Identity.Web.Test.csproj @@ -26,7 +26,6 @@ - From 5c12e8d8e36f878ba23d1623acb345e79d4ea045 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Tue, 4 Feb 2025 19:04:46 -0800 Subject: [PATCH 20/36] Initial setup of extension classes --- Microsoft.Identity.Web.sln | 7 +++ .../DevApps/aspnet-mvc/OwinWebApi/Web.config | 14 +++--- .../DevApps/aspnet-mvc/OwinWebApp/Web.config | 14 +++--- ...CustomSignedAssertionProviderExtensions.cs | 17 +++++++ .../CustomSignedAssertionProviderTests.csproj | 30 +++++++++++++ .../MyCustomSignedAssertionLoader.cs | 45 +++++++++++++++++++ .../MyCustomSignedAssertionProvider.cs | 26 +++++++++++ .../UnitTest1.cs | 14 ++++++ 8 files changed, 153 insertions(+), 14 deletions(-) create mode 100644 tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensions.cs create mode 100644 tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderTests.csproj create mode 100644 tests/E2E Tests/CustomSignedAssertionProviderTests/MyCustomSignedAssertionLoader.cs create mode 100644 tests/E2E Tests/CustomSignedAssertionProviderTests/MyCustomSignedAssertionProvider.cs create mode 100644 tests/E2E Tests/CustomSignedAssertionProviderTests/UnitTest1.cs 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/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/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..f4534b750 --- /dev/null +++ b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderTests.csproj @@ -0,0 +1,30 @@ + + + + net8.0;net9.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/E2E Tests/CustomSignedAssertionProviderTests/MyCustomSignedAssertionLoader.cs b/tests/E2E Tests/CustomSignedAssertionProviderTests/MyCustomSignedAssertionLoader.cs new file mode 100644 index 000000000..8980016ad --- /dev/null +++ b/tests/E2E Tests/CustomSignedAssertionProviderTests/MyCustomSignedAssertionLoader.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +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) + { + _logger.LogError(22, ex.Message); + _logger.LogError(42, "Failed to get signed assertion."); + 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..e35db0c0a --- /dev/null +++ b/tests/E2E Tests/CustomSignedAssertionProviderTests/MyCustomSignedAssertionProvider.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Identity.Client; +using Microsoft.Identity.Web; + +namespace CustomSignedAssertionProviderTests +{ + internal class MyCustomSignedAssertionProvider : ClientAssertionProviderBase + { + public MyCustomSignedAssertionProvider(Dictionary? properties) + { + // Implement the logic to extract what you need from the properties passed in the configuration + } + + protected override Task GetClientAssertionAsync(AssertionRequestOptions? assertionRequestOptions) + { + // Implement the logic to get the signed assertion, which is probably going to be a call to a service. + // This call can be parameterized by the parameters in the properties of 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/UnitTest1.cs b/tests/E2E Tests/CustomSignedAssertionProviderTests/UnitTest1.cs new file mode 100644 index 000000000..53cd7d1fd --- /dev/null +++ b/tests/E2E Tests/CustomSignedAssertionProviderTests/UnitTest1.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace CustomSignedAssertionProviderTests +{ + public class UnitTest1 + { + [Fact] + public void Test1() + { + + } + } +} From 6779b3d21b429b79b73e0da65a4a63ef4441b487 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Wed, 5 Feb 2025 11:57:06 -0800 Subject: [PATCH 21/36] added snk reference --- .../CustomSignedAssertionProviderTests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderTests.csproj b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderTests.csproj index f4534b750..ea8d363c4 100644 --- a/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderTests.csproj +++ b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderTests.csproj @@ -2,7 +2,7 @@ net8.0;net9.0 - enable + ../../../build/MSAL.snk enable false From 6748f026417121fbe22e80c22c802f07f1e4269d Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Wed, 5 Feb 2025 13:33:38 -0800 Subject: [PATCH 22/36] Added test --- ...gnedAssertionProviderExtensibilityTests.cs | 39 +++++++++++++++++++ .../CustomSignedAssertionProviderTests.csproj | 3 +- .../MyCustomSignedAssertionLoader.cs | 5 ++- .../MyCustomSignedAssertionProvider.cs | 9 +++-- .../UnitTest1.cs | 14 ------- .../GraphServiceClientTests.csproj | 1 + 6 files changed, 51 insertions(+), 20 deletions(-) create mode 100644 tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs delete mode 100644 tests/E2E Tests/CustomSignedAssertionProviderTests/UnitTest1.cs diff --git a/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs new file mode 100644 index 000000000..f93297d0c --- /dev/null +++ b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Web; + + +namespace CustomSignedAssertionProviderTests +{ + public class CustomSignedAssertionProviderExtensibilityTests + { + [Fact] + public async Task UseSignedAssertionFromCustomSignedAssertionProvider() + { + TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); + 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" + }]; + }); + tokenAcquirerFactory.Services.AddCustomSignedAssertionProvider(); + var serviceProvider = tokenAcquirerFactory.Build(); + + // Get the authorization request creator service + IAuthorizationHeaderProvider authorizationHeaderProvider = serviceProvider.GetRequiredService(); + + string authorizationHeader = await authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync("https://graph.microsoft.com/.default"); + //Console.WriteLine(authorizationHeader.Substring(0, authorizationHeader.IndexOf(" ", StringComparison.OrdinalIgnoreCase) + 4) + "..."); + } + } +} diff --git a/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderTests.csproj b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderTests.csproj index ea8d363c4..09fff4db9 100644 --- a/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderTests.csproj +++ b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderTests.csproj @@ -1,7 +1,7 @@ - net8.0;net9.0 + net9.0 ../../../build/MSAL.snk enable false @@ -19,6 +19,7 @@ + diff --git a/tests/E2E Tests/CustomSignedAssertionProviderTests/MyCustomSignedAssertionLoader.cs b/tests/E2E Tests/CustomSignedAssertionProviderTests/MyCustomSignedAssertionLoader.cs index 8980016ad..bf2f63e5a 100644 --- a/tests/E2E Tests/CustomSignedAssertionProviderTests/MyCustomSignedAssertionLoader.cs +++ b/tests/E2E Tests/CustomSignedAssertionProviderTests/MyCustomSignedAssertionLoader.cs @@ -1,6 +1,8 @@ // 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; @@ -35,8 +37,7 @@ public async Task LoadIfNeededAsync(CredentialDescription credentialDescription, } catch (Exception ex) { - _logger.LogError(22, ex.Message); - _logger.LogError(42, "Failed to get signed assertion."); + _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 index e35db0c0a..3500e6e00 100644 --- a/tests/E2E Tests/CustomSignedAssertionProviderTests/MyCustomSignedAssertionProvider.cs +++ b/tests/E2E Tests/CustomSignedAssertionProviderTests/MyCustomSignedAssertionProvider.cs @@ -1,6 +1,9 @@ // 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; @@ -10,13 +13,13 @@ internal class MyCustomSignedAssertionProvider : ClientAssertionProviderBase { public MyCustomSignedAssertionProvider(Dictionary? properties) { - // Implement the logic to extract what you need from the properties passed in the configuration + // Implement logic to extract information from the properties passed in the configuration. } protected override Task GetClientAssertionAsync(AssertionRequestOptions? assertionRequestOptions) { - // Implement the logic to get the signed assertion, which is probably going to be a call to a service. - // This call can be parameterized by the parameters in the properties of the constructor. + // 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); diff --git a/tests/E2E Tests/CustomSignedAssertionProviderTests/UnitTest1.cs b/tests/E2E Tests/CustomSignedAssertionProviderTests/UnitTest1.cs deleted file mode 100644 index 53cd7d1fd..000000000 --- a/tests/E2E Tests/CustomSignedAssertionProviderTests/UnitTest1.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace CustomSignedAssertionProviderTests -{ - public class UnitTest1 - { - [Fact] - public void Test1() - { - - } - } -} diff --git a/tests/E2E Tests/GraphServiceClientTests/GraphServiceClientTests.csproj b/tests/E2E Tests/GraphServiceClientTests/GraphServiceClientTests.csproj index 9d2b3e325..db1b0fe77 100644 --- a/tests/E2E Tests/GraphServiceClientTests/GraphServiceClientTests.csproj +++ b/tests/E2E Tests/GraphServiceClientTests/GraphServiceClientTests.csproj @@ -18,6 +18,7 @@ + From 06d6700dbeeeb57db2a1110ad596a2e1befb6b9b Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Wed, 5 Feb 2025 14:08:27 -0800 Subject: [PATCH 23/36] Added handling in ConfidentialClientApplicationBuilderExtension for CredentialSource.CustomSignedAssertion --- ...lientApplicationBuilderExtension.Logger.cs | 20 ++++++++++++++++--- ...entialClientApplicationBuilderExtension.cs | 8 ++++++++ .../LoggingEventId.cs | 1 + .../net462/InternalAPI.Unshipped.txt | 2 ++ .../net472/InternalAPI.Unshipped.txt | 2 ++ .../net6.0/InternalAPI.Unshipped.txt | 2 ++ .../net7.0/InternalAPI.Unshipped.txt | 2 ++ .../net8.0/InternalAPI.Unshipped.txt | 2 ++ .../net9.0/InternalAPI.Unshipped.txt | 2 ++ .../netstandard2.0/InternalAPI.Unshipped.txt | 2 ++ ...gnedAssertionProviderExtensibilityTests.cs | 7 +++++-- 11 files changed, 45 insertions(+), 5 deletions(-) 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/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs index f93297d0c..b61f879a5 100644 --- a/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs +++ b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Client; using Microsoft.Identity.Web; @@ -32,8 +33,10 @@ public async Task UseSignedAssertionFromCustomSignedAssertionProvider() // Get the authorization request creator service IAuthorizationHeaderProvider authorizationHeaderProvider = serviceProvider.GetRequiredService(); - string authorizationHeader = await authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync("https://graph.microsoft.com/.default"); - //Console.WriteLine(authorizationHeader.Substring(0, authorizationHeader.IndexOf(" ", StringComparison.OrdinalIgnoreCase) + 4) + "..."); + await Assert.ThrowsAsync(async () => + { + await authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync("https://graph.microsoft.com/.default"); + }); } } } From db5546c25cf876cbf65219424401b493244b433d Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Wed, 5 Feb 2025 18:48:12 -0800 Subject: [PATCH 24/36] removing unneeded project reference --- .../GraphServiceClientTests/GraphServiceClientTests.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/E2E Tests/GraphServiceClientTests/GraphServiceClientTests.csproj b/tests/E2E Tests/GraphServiceClientTests/GraphServiceClientTests.csproj index db1b0fe77..9d2b3e325 100644 --- a/tests/E2E Tests/GraphServiceClientTests/GraphServiceClientTests.csproj +++ b/tests/E2E Tests/GraphServiceClientTests/GraphServiceClientTests.csproj @@ -18,7 +18,6 @@ - From c32179a233a6c2e7c0b02ac25a7ab65f2977793a Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Wed, 5 Feb 2025 21:27:24 -0800 Subject: [PATCH 25/36] bring constructor up through DefaultCredentialsLoader --- .../DefaultCertificateLoader.cs | 9 +++++++++ 1 file changed, 9 insertions(+) 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). /// From bdd73933b95a67b7e98020362697ad4be27532b5 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Wed, 5 Feb 2025 21:41:33 -0800 Subject: [PATCH 26/36] added more configuration --- ...efaultCredentialsLoader.CustomSignedAssertion.cs | 1 - .../PublicAPI.Unshipped.txt | 1 + ...stomSignedAssertionProviderExtensibilityTests.cs | 3 ++- .../MyCustomSignedAssertionLoader.cs | 5 ++++- .../appsettings.json | 13 +++++++++++++ 5 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 tests/E2E Tests/CustomSignedAssertionProviderTests/appsettings.json diff --git a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs index 6fe647a44..defc1ceea 100644 --- a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs +++ b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs @@ -34,7 +34,6 @@ public DefaultCredentialsLoader(IEnumerable cust sourceLoaderDict.Add(providerName, provider); } } - CustomSignedAssertionCredentialSourceLoaders = sourceLoaderDict; } diff --git a/src/Microsoft.Identity.Web.Certificate/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.Certificate/PublicAPI.Unshipped.txt index 477b39e65..862e7185a 100644 --- a/src/Microsoft.Identity.Web.Certificate/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.Certificate/PublicAPI.Unshipped.txt @@ -1,3 +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/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs index b61f879a5..2c264faf8 100644 --- a/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs +++ b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Identity.Abstractions; @@ -28,7 +29,7 @@ public async Task UseSignedAssertionFromCustomSignedAssertionProvider() }]; }); tokenAcquirerFactory.Services.AddCustomSignedAssertionProvider(); - var serviceProvider = tokenAcquirerFactory.Build(); + IServiceProvider serviceProvider = tokenAcquirerFactory.Build(); // Get the authorization request creator service IAuthorizationHeaderProvider authorizationHeaderProvider = serviceProvider.GetRequiredService(); diff --git a/tests/E2E Tests/CustomSignedAssertionProviderTests/MyCustomSignedAssertionLoader.cs b/tests/E2E Tests/CustomSignedAssertionProviderTests/MyCustomSignedAssertionLoader.cs index bf2f63e5a..d75b2cb46 100644 --- a/tests/E2E Tests/CustomSignedAssertionProviderTests/MyCustomSignedAssertionLoader.cs +++ b/tests/E2E Tests/CustomSignedAssertionProviderTests/MyCustomSignedAssertionLoader.cs @@ -37,7 +37,10 @@ public async Task LoadIfNeededAsync(CredentialDescription credentialDescription, } catch (Exception ex) { - _logger.LogError(42, "Failed to get signed assertion from {ProviderName}. exception occurred: {Message}. Setting skip to true.", credentialDescription.CustomSignedAssertionProviderName, ex.Message); + 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/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" + } + ] + } +} From 9a3f44f33f60a3284dea39b31bc4f61bbc0ba6a8 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Wed, 5 Feb 2025 21:53:30 -0800 Subject: [PATCH 27/36] addressing PR feedback --- ...faultCredentialsLoader.CustomSignedAssertion.cs | 14 +++++--------- .../DefaultCredentialsLoader.Logger.cs | 2 +- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs index defc1ceea..b6ee79bc3 100644 --- a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs +++ b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs @@ -16,7 +16,7 @@ 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); @@ -43,30 +43,26 @@ public DefaultCredentialsLoader(IEnumerable cust /// protected IDictionary? CustomSignedAssertionCredentialSourceLoaders { get; } - private async Task ProcessCustomSignedAssertionAsync(CredentialDescription credentialDescription, CredentialSourceLoaderParameters? parameters) { - // No source loader(s) if (CustomSignedAssertionCredentialSourceLoaders == null || CustomSignedAssertionCredentialSourceLoaders.Count == 0) { + // No source loader(s) _logger.LogError(CertificateErrorMessage.CustomProviderSourceLoaderNullOrEmpty); } - - // No provider name else if (string.IsNullOrEmpty(credentialDescription.CustomSignedAssertionProviderName)) { + // No provider name _logger.LogError(CertificateErrorMessage.CustomProviderNameNullOrEmpty); } - - // No source loader for provider name else if (!CustomSignedAssertionCredentialSourceLoaders!.TryGetValue(credentialDescription.CustomSignedAssertionProviderName!, out ICustomSignedAssertionProvider? sourceLoader)) { + // No source loader for provider name _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 { + // 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); diff --git a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.Logger.cs b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.Logger.cs index afeb05dd6..79aeca0c0 100644 --- a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.Logger.cs +++ b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.Logger.cs @@ -34,7 +34,7 @@ public static void CredentialLoadingFailure(ILogger logger, CredentialDescriptio private static readonly Action s_customSignedAssertionProviderLoadingFailure = LoggerMessage.Define( LogLevel.Information, - new EventId(7, nameof(CustomSignedAssertionProviderLoadingFailure)), + new EventId(8, nameof(CustomSignedAssertionProviderLoadingFailure)), CustomSignedAssertionProviderLoadingFailureMessage("{name}", "{sourceType}", "{skip}") ); From c5d15b7c3a7be00036dffba435d1ba12a2e60a7d Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Wed, 5 Feb 2025 23:09:11 -0800 Subject: [PATCH 28/36] Refactored Tests --- ...gnedAssertionProviderExtensibilityTests.cs | 20 +- .../CustomSignedAssertionProviderTests.cs | 188 +++++++++--------- 2 files changed, 112 insertions(+), 96 deletions(-) diff --git a/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs index 2c264faf8..e91cd24fc 100644 --- a/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs +++ b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs @@ -17,6 +17,8 @@ public class CustomSignedAssertionProviderExtensibilityTests [Fact] public async Task UseSignedAssertionFromCustomSignedAssertionProvider() { + // Arrange + string expectedExceptionCode = "AADSTS50027"; TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); tokenAcquirerFactory.Services.Configure(options => { @@ -30,14 +32,22 @@ public async Task UseSignedAssertionFromCustomSignedAssertionProvider() }); tokenAcquirerFactory.Services.AddCustomSignedAssertionProvider(); IServiceProvider serviceProvider = tokenAcquirerFactory.Build(); - - // Get the authorization request creator service IAuthorizationHeaderProvider authorizationHeaderProvider = serviceProvider.GetRequiredService(); - await Assert.ThrowsAsync(async () => + try { - await authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync("https://graph.microsoft.com/.default"); - }); + // Act + _ = await authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync("https://graph.microsoft.com/.default"); + } + catch (MsalServiceException MsalEx) + { + // Assert + Assert.Contains(expectedExceptionCode, MsalEx.Message, StringComparison.InvariantCulture); + } + catch (Exception ex) + { + Assert.Fail(ex.Message); + } } } } diff --git a/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs b/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs index cf080f967..7b89e2482 100644 --- a/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs +++ b/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs @@ -23,144 +23,150 @@ public void Constructor_NullProviders_ThrowsArgumentNullException() } [Theory] - [MemberData(nameof(CustomSignedAssertionLoggingTestData))] - public async Task ProcessCustomSignedAssertionAsync_Tests( - List providerList, - CredentialDescription credentialDescription, - LogLevel expectedLogLevel = LogLevel.None, - string? expectedLogMessage = null, - string? expectedExceptionMessage = null) + [MemberData(nameof(CustomSignedAssertionProviderLoggingTestData), DisableDiscoveryEnumeration = true)] + public async Task ProcessCustomSignedAssertionAsync_Tests(CustomSignedAssertionProviderTheoryData data) { // Arrange var loggerMock = new CustomMockLogger(); - var loader = new DefaultCredentialsLoader(providerList, loggerMock); + var loader = new DefaultCredentialsLoader(data.AssertionProviderList, loggerMock); // Act try { - await loader.LoadCredentialsIfNeededAsync(credentialDescription, null); + await loader.LoadCredentialsIfNeededAsync(data.CredentialDescription, null); } catch (Exception ex) { - Assert.Equal(expectedExceptionMessage, ex.Message); + Assert.Equal(data.ExpectedExceptionMessage, ex.Message); // This is validating the logging behavior defined by DefaultCredentialsLoader.Logger.CustomSignedAssertionProviderLoadingFailure - if (expectedLogMessage is not null) + if (data.ExpectedLogMessage is not null) { - Assert.Contains(loggerMock.LoggedMessages, log => log.LogLevel == expectedLogLevel && log.Message.Contains(expectedLogMessage, StringComparison.InvariantCulture)); + Assert.Contains(loggerMock.LoggedMessages, log => log.LogLevel == data.ExpectedLogLevel && log.Message.Contains(data.ExpectedLogMessage, StringComparison.InvariantCulture)); } return; } // Assert - if (expectedLogMessage != null) + if (data.ExpectedLogMessage is not null) { - Assert.Contains(loggerMock.LoggedMessages, log => log.LogLevel == expectedLogLevel && log.Message.Contains(expectedLogMessage, StringComparison.InvariantCulture)); + Assert.Contains(loggerMock.LoggedMessages, log => log.LogLevel == data.ExpectedLogLevel && log.Message.Contains(data.ExpectedLogMessage, StringComparison.InvariantCulture)); } else { - Assert.DoesNotContain(loggerMock.LoggedMessages, log => log.LogLevel == expectedLogLevel); + Assert.DoesNotContain(loggerMock.LoggedMessages, log => log.LogLevel == data.ExpectedLogLevel); } } - public static IEnumerable CustomSignedAssertionLoggingTestData() + public static TheoryData CustomSignedAssertionProviderLoggingTestData() { - // No source loaders - yield return new object[] - { - new List(), - new CredentialDescription + return + [ + // No source loaders + new CustomSignedAssertionProviderTheoryData { - CustomSignedAssertionProviderName = "Provider1", - SourceType = CredentialSource.CustomSignedAssertion, - Skip = false + AssertionProviderList = [], + CredentialDescription = new CredentialDescription + { + CustomSignedAssertionProviderName = "Provider1", + SourceType = CredentialSource.CustomSignedAssertion, + Skip = false + }, + ExpectedLogLevel = LogLevel.Error, + ExpectedLogMessage = CertificateErrorMessage.CustomProviderSourceLoaderNullOrEmpty }, - LogLevel.Error, - CertificateErrorMessage.CustomProviderSourceLoaderNullOrEmpty - }; - // No provider name given - yield return new object[] - { - new List { new SuccessfulCustomSignedAssertionProvider("Provider2") }, - new CredentialDescription + // No provider name given + new CustomSignedAssertionProviderTheoryData { - CustomSignedAssertionProviderName = null, - SourceType = CredentialSource.CustomSignedAssertion + AssertionProviderList = [new SuccessfulCustomSignedAssertionProvider("Provider2")], + CredentialDescription = new CredentialDescription + { + CustomSignedAssertionProviderName = null, + SourceType = CredentialSource.CustomSignedAssertion + }, + ExpectedLogLevel = LogLevel.Error, + ExpectedLogMessage = CertificateErrorMessage.CustomProviderNameNullOrEmpty }, - LogLevel.Error, - CertificateErrorMessage.CustomProviderNameNullOrEmpty - }; - // Given provider name not found - yield return new object[] - { - new List { new SuccessfulCustomSignedAssertionProvider("NotProvider3") }, - new CredentialDescription + // Given provider name not found + new CustomSignedAssertionProviderTheoryData { - CustomSignedAssertionProviderName = "Provider3", - SourceType = CredentialSource.CustomSignedAssertion + AssertionProviderList = [new SuccessfulCustomSignedAssertionProvider("NotProvider3")], + CredentialDescription = new CredentialDescription + { + CustomSignedAssertionProviderName = "Provider3", + SourceType = CredentialSource.CustomSignedAssertion + }, + ExpectedLogLevel = LogLevel.Error, + ExpectedLogMessage = string.Format(CultureInfo.InvariantCulture, CertificateErrorMessage.CustomProviderNotFound, "Provider3") }, - LogLevel.Error, - string.Format(CultureInfo.InvariantCulture, CertificateErrorMessage.CustomProviderNotFound, "Provider3") - }; - // Happy path (no logging expected) - yield return new object[] - { - new List { new SuccessfulCustomSignedAssertionProvider("Provider4") }, - new CredentialDescription + // Happy path (no logging expected) + new CustomSignedAssertionProviderTheoryData { - CustomSignedAssertionProviderName = "Provider4", - SourceType = CredentialSource.CustomSignedAssertion - } - }; + AssertionProviderList = [new SuccessfulCustomSignedAssertionProvider("Provider4")], + CredentialDescription = new CredentialDescription + { + CustomSignedAssertionProviderName = "Provider4", + SourceType = CredentialSource.CustomSignedAssertion + } + }, - // CustomSignedAssertionProvider (i.e. the user's extension) throws an exception - CredentialDescription providerFiveCredDesc = new() + // CustomSignedAssertionProvider (i.e. the user's extension) throws an exception + new CustomSignedAssertionProviderTheoryData { - CustomSignedAssertionProviderName = "Provider5", - SourceType = CredentialSource.CustomSignedAssertion - }; - - yield return new object[] - { - new List { new FailingCustomSignedAssertionProvider("Provider5") }, - providerFiveCredDesc, - LogLevel.Information, - string.Format - ( - CultureInfo.InvariantCulture, - DefaultCredentialsLoader.CustomSignedAssertionProviderLoadingFailureMessage + AssertionProviderList = [new FailingCustomSignedAssertionProvider("Provider5")], + CredentialDescription = new CredentialDescription + { + CustomSignedAssertionProviderName = "Provider5", + SourceType = CredentialSource.CustomSignedAssertion + }, + ExpectedLogLevel = LogLevel.Information, + ExpectedLogMessage = string.Format ( - providerFiveCredDesc.CustomSignedAssertionProviderName ?? DefaultCredentialsLoader.nameMissing, - providerFiveCredDesc.SourceType.ToString(), - providerFiveCredDesc.Skip.ToString() - ) - ), - FailingCustomSignedAssertionProvider.ExceptionMessage - }; - - // Multiple providers with the same name - yield return new object[] - { - new List { new SuccessfulCustomSignedAssertionProvider("Provider6"), new SuccessfulCustomSignedAssertionProvider("Provider6") }, - new CredentialDescription - { - CustomSignedAssertionProviderName = "Provider6", - SourceType = CredentialSource.CustomSignedAssertion + CultureInfo.InvariantCulture, + DefaultCredentialsLoader.CustomSignedAssertionProviderLoadingFailureMessage + ( + "Provider5", + CredentialSource.CustomSignedAssertion.ToString(), + false.ToString() + ) + ), + ExpectedExceptionMessage = FailingCustomSignedAssertionProvider.ExceptionMessage }, - LogLevel.Warning, - string.Format(CultureInfo.InvariantCulture, CertificateErrorMessage.CustomProviderNameAlreadyExists, "Provider6") - }; + + // 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; } = new List(); + public List LoggedMessages { get; } = []; IDisposable ILogger.BeginScope(TState state) => null!; From 6e55b7a666fd7ab3de65bc921c7fd6f9f698bef1 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Thu, 6 Feb 2025 11:54:36 -0800 Subject: [PATCH 29/36] added appsettings copy to csproj --- .../CustomSignedAssertionProviderTests.csproj | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderTests.csproj b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderTests.csproj index 09fff4db9..eaa8271e0 100644 --- a/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderTests.csproj +++ b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderTests.csproj @@ -7,6 +7,16 @@ false + + + + + + + Always + + + From 7a47370ca89e18834e802e125aad9889f2a254c9 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Thu, 6 Feb 2025 12:17:43 -0800 Subject: [PATCH 30/36] Remove duplicitive functionality in test project --- .../CustomSignedAssertionProviderExtensibilityTests.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs index e91cd24fc..8d5d29745 100644 --- a/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs +++ b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs @@ -20,7 +20,10 @@ public async Task UseSignedAssertionFromCustomSignedAssertionProvider() // Arrange string expectedExceptionCode = "AADSTS50027"; TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); - tokenAcquirerFactory.Services.Configure(options => + tokenAcquirerFactory.Services.AddCustomSignedAssertionProvider(); + + // this is how the authentication options can be configured in code rather than in the appsettnigs file +/* tokenAcquirerFactory.Services.Configure(options => { options.Instance = "https://login.microsoftonline.com/"; options.TenantId = "msidlab4.onmicrosoft.com"; @@ -29,8 +32,7 @@ public async Task UseSignedAssertionFromCustomSignedAssertionProvider() SourceType = CredentialSource.CustomSignedAssertion, CustomSignedAssertionProviderName = "MyCustomExtension" }]; - }); - tokenAcquirerFactory.Services.AddCustomSignedAssertionProvider(); + });*/ IServiceProvider serviceProvider = tokenAcquirerFactory.Build(); IAuthorizationHeaderProvider authorizationHeaderProvider = serviceProvider.GetRequiredService(); From ebc7128489ccdb52b70fcd60c413f4a502ea8e60 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Thu, 6 Feb 2025 12:19:02 -0800 Subject: [PATCH 31/36] fix typo --- .../CustomSignedAssertionProviderExtensibilityTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs index 8d5d29745..228e299e3 100644 --- a/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs +++ b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs @@ -22,7 +22,8 @@ public async Task UseSignedAssertionFromCustomSignedAssertionProvider() TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); tokenAcquirerFactory.Services.AddCustomSignedAssertionProvider(); - // this is how the authentication options can be configured in code rather than in the appsettnigs file + // 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/"; From 260e006d8c73c066c6523c38acd805264d30ba35 Mon Sep 17 00:00:00 2001 From: JLoze <103777376+JoshLozensky@users.noreply.github.com> Date: Thu, 6 Feb 2025 12:56:58 -0800 Subject: [PATCH 32/36] formatting Co-authored-by: Keegan --- .../CustomSignedAssertionProviderExtensibilityTests.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs index 228e299e3..03f706eef 100644 --- a/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs +++ b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs @@ -24,7 +24,8 @@ public async Task UseSignedAssertionFromCustomSignedAssertionProvider() // 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 => + /* + tokenAcquirerFactory.Services.Configure(options => { options.Instance = "https://login.microsoftonline.com/"; options.TenantId = "msidlab4.onmicrosoft.com"; @@ -33,7 +34,8 @@ public async Task UseSignedAssertionFromCustomSignedAssertionProvider() SourceType = CredentialSource.CustomSignedAssertion, CustomSignedAssertionProviderName = "MyCustomExtension" }]; - });*/ + }); + */ IServiceProvider serviceProvider = tokenAcquirerFactory.Build(); IAuthorizationHeaderProvider authorizationHeaderProvider = serviceProvider.GetRequiredService(); From 0de59705d77db045e871241f22e1565c68e68111 Mon Sep 17 00:00:00 2001 From: JLoze <103777376+JoshLozensky@users.noreply.github.com> Date: Thu, 6 Feb 2025 12:57:28 -0800 Subject: [PATCH 33/36] Update tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs Co-authored-by: Keegan --- .../CustomSignedAssertionProviderExtensibilityTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs index 03f706eef..0178c6b18 100644 --- a/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs +++ b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs @@ -49,7 +49,7 @@ public async Task UseSignedAssertionFromCustomSignedAssertionProvider() // Assert Assert.Contains(expectedExceptionCode, MsalEx.Message, StringComparison.InvariantCulture); } - catch (Exception ex) + catch (Exception ex) when (e is not XunitException) { Assert.Fail(ex.Message); } From 42f611a4c89470ffefa356944028813f83455af1 Mon Sep 17 00:00:00 2001 From: JLoze <103777376+JoshLozensky@users.noreply.github.com> Date: Thu, 6 Feb 2025 12:58:37 -0800 Subject: [PATCH 34/36] Update tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderTests.csproj Co-authored-by: Keegan --- .../CustomSignedAssertionProviderTests.csproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderTests.csproj b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderTests.csproj index eaa8271e0..2cb4fef1d 100644 --- a/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderTests.csproj +++ b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderTests.csproj @@ -7,10 +7,6 @@ false - - - - Always From 6a45dca71e9149fbd7bba3b33fa572e3977328cb Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Thu, 6 Feb 2025 13:00:51 -0800 Subject: [PATCH 35/36] updated comment --- .../DefaultCredentialsLoader.CustomSignedAssertion.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs index b6ee79bc3..2d9a22fd9 100644 --- a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs +++ b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs @@ -38,7 +38,7 @@ public DefaultCredentialsLoader(IEnumerable cust } /// - /// Dictionary of custom signed assertion credential source loaders, by name (fully qualified type name). + /// 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; } From 2ed61140d5b237f5309eabff19d43cd13b0fc436 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Thu, 6 Feb 2025 13:09:14 -0800 Subject: [PATCH 36/36] fix typo --- .../CustomSignedAssertionProviderExtensibilityTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs index 0178c6b18..158957f21 100644 --- a/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs +++ b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs @@ -8,6 +8,7 @@ using Microsoft.Identity.Abstractions; using Microsoft.Identity.Client; using Microsoft.Identity.Web; +using Xunit.Sdk; namespace CustomSignedAssertionProviderTests @@ -49,7 +50,7 @@ public async Task UseSignedAssertionFromCustomSignedAssertionProvider() // Assert Assert.Contains(expectedExceptionCode, MsalEx.Message, StringComparison.InvariantCulture); } - catch (Exception ex) when (e is not XunitException) + catch (Exception ex) when (ex is not XunitException) { Assert.Fail(ex.Message); }