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