Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
8ca8810
Added logic to DefaultCredentialsLoader to support custom signed asse…
JoshLozensky Jan 30, 2025
adc6ec9
adjusted logging message
JoshLozensky Jan 30, 2025
b82dd8b
simplified constructor
JoshLozensky Jan 30, 2025
129819b
Added unit tests
JoshLozensky Jan 30, 2025
eeb9d7d
Update src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoade…
JoshLozensky Jan 30, 2025
b4096af
reworked error logging
JoshLozensky Jan 31, 2025
c9ffdef
Update error string
JoshLozensky Feb 4, 2025
6cd4fdc
extra line
JoshLozensky Feb 4, 2025
f7dfa87
fixing public API and addressing PR comments
JoshLozensky Feb 4, 2025
4be7c48
changed CustomSignedAssertionCredentialSourceLoader dict to use ICust…
JoshLozensky Feb 4, 2025
39fdc69
finished unit test for behavior when user extension throws an error
JoshLozensky Feb 4, 2025
fb145fd
added to method summary
JoshLozensky Feb 4, 2025
51681b4
Changed to concurrent dict and added logging for duplicate keys
JoshLozensky Feb 4, 2025
79fc329
Added more specificity to tests and also added a check for duplicate …
JoshLozensky Feb 4, 2025
9aaaac0
Merge branch 'master' into lozensky/AddCustomSignedAssertionExtensibi…
JoshLozensky Feb 4, 2025
8520c07
Added null check and test
JoshLozensky Feb 5, 2025
c6ca484
Added custom mock logger to unit tests
JoshLozensky Feb 5, 2025
cb9affe
changed CustomSignedAssertionCredentialSourceLoaders to protected
JoshLozensky Feb 5, 2025
b8edcb6
improve null check
JoshLozensky Feb 5, 2025
2a02584
removed Moq dependency
JoshLozensky Feb 5, 2025
5c12e8d
Initial setup of extension classes
JoshLozensky Feb 5, 2025
6779b3d
added snk reference
JoshLozensky Feb 5, 2025
6748f02
Added test
JoshLozensky Feb 5, 2025
06d6700
Added handling in ConfidentialClientApplicationBuilderExtension for C…
JoshLozensky Feb 5, 2025
db5546c
removing unneeded project reference
JoshLozensky Feb 6, 2025
c32179a
bring constructor up through DefaultCredentialsLoader
JoshLozensky Feb 6, 2025
bdd7393
added more configuration
JoshLozensky Feb 6, 2025
9a3f44f
addressing PR feedback
JoshLozensky Feb 6, 2025
c5d15b7
Refactored Tests
JoshLozensky Feb 6, 2025
6e55b7a
added appsettings copy to csproj
JoshLozensky Feb 6, 2025
7a47370
Remove duplicitive functionality in test project
JoshLozensky Feb 6, 2025
ebc7128
fix typo
JoshLozensky Feb 6, 2025
260e006
formatting
JoshLozensky Feb 6, 2025
0de5970
Update tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSigne…
JoshLozensky Feb 6, 2025
42f611a
Update tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSigne…
JoshLozensky Feb 6, 2025
6a45dca
updated comment
JoshLozensky Feb 6, 2025
2ed6114
fix typo
JoshLozensky Feb 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Microsoft.Identity.Web.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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. ";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ public DefaultCertificateLoader() : this(null)
{
}

/// <summary>
/// Constructor with custom signed assertion providers.
/// </summary>
/// <param name="customSignedAssertionProviders">List of providers of custom signed assertions</param>
/// <param name="logger">ILogger.</param>
public DefaultCertificateLoader(IEnumerable<ICustomSignedAssertionProvider> customSignedAssertionProviders, ILogger<DefaultCertificateLoader>? logger) : base(customSignedAssertionProviders, logger)
{
}

/// <summary>
/// This default is overridable at the level of the credential description (for the certificate from KeyVault).
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Constructor for DefaultCredentialsLoader when using custom signed assertion provider source loaders.
/// </summary>
/// <param name="customSignedAssertionProviders">Set of custom signed assertion providers.</param>
/// <param name="logger">ILogger.</param>
public DefaultCredentialsLoader(IEnumerable<ICustomSignedAssertionProvider> customSignedAssertionProviders, ILogger<DefaultCredentialsLoader>? logger) : this(logger)
{
_ = Throws.IfNull(customSignedAssertionProviders);
var sourceLoaderDict = new Dictionary<string, ICustomSignedAssertionProvider>();

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

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

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;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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}.";
}

/// <summary>
/// Logging infrastructure
/// </summary>
Expand All @@ -18,13 +24,25 @@ private static class Logger
private static readonly Action<ILogger, string, string, bool, Exception?> s_credentialLoadingFailure =
LoggerMessage.Define<string, string, bool>(
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<ILogger, string, string, bool, Exception?> s_customSignedAssertionProviderLoadingFailure =
LoggerMessage.Define<string, string, bool>(
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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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!
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
#nullable enable
Microsoft.Identity.Web.DefaultCertificateLoader.DefaultCertificateLoader(System.Collections.Generic.IEnumerable<Microsoft.Identity.Abstractions.ICustomSignedAssertionProvider!>! customSignedAssertionProviders, Microsoft.Extensions.Logging.ILogger<Microsoft.Identity.Web.DefaultCertificateLoader!>? logger) -> void
Microsoft.Identity.Web.DefaultCredentialsLoader.CustomSignedAssertionCredentialSourceLoaders.get -> System.Collections.Generic.IDictionary<string!, Microsoft.Identity.Abstractions.ICustomSignedAssertionProvider!>?
Microsoft.Identity.Web.DefaultCredentialsLoader.DefaultCredentialsLoader(System.Collections.Generic.IEnumerable<Microsoft.Identity.Abstractions.ICustomSignedAssertionProvider!>! customSignedAssertionProviders, Microsoft.Extensions.Logging.ILogger<Microsoft.Identity.Web.DefaultCredentialsLoader!>? logger) -> void
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ internal static class Logger
LoggingEventId.UsingSignedAssertionFromVault,
"[MsIdWeb] Using signed assertion from {signedAssertionUri} as client credentials. ");

private static readonly Action<ILogger, string, Exception?> s_usingSignedAssertionFromCustomProvider =
LoggerMessage.Define<string>(
LogLevel.Information,
LoggingEventId.UsingSignedAssertionFromCustomProvider,
"[MsIdWeb] Using signed assertion from {signedAssertionUri} as client credentials. ");

private static readonly Action<ILogger, string, Exception?> s_usingCertThumbprint =
LoggerMessage.Define<string>(
LogLevel.Information,
Expand All @@ -49,9 +55,9 @@ internal static class Logger

private static readonly Action<ILogger, string, string, Exception?> s_credentialAttemptFailed =
LoggerMessage.Define<string, string>(
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}.");

/// <summary>
/// Logger for attempting to use a CredentialDescription with MSAL
Expand Down Expand Up @@ -131,6 +137,14 @@ public static void UsingSignedAssertionFromVault(
ILogger logger,
string signedAssertionUri) => s_usingSignedAssertionFromVault(logger, signedAssertionUri, default!);

/// <summary>
/// Logger for handling information specific to ConfidentialClientApplicationBuilderExtension.
/// </summary>
/// <param name="logger">ILogger.</param>
/// <param name="signedAssertionUri"></param>
public static void UsingSignedAssertionFromCustomProvider(
ILogger logger,
string signedAssertionUri) => s_usingSignedAssertionFromCustomProvider(logger, signedAssertionUri, default!);

/// <summary>
/// Logger for handling information specific to ConfidentialClientApplicationBuilderExtension.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,14 @@ public static async Task<ConfidentialClientApplicationBuilder> WithClientCredent
return credential;
}
}
if (credential.SourceType == CredentialSource.CustomSignedAssertion)
{
if (!credential.Skip)
{
Logger.UsingSignedAssertionFromCustomProvider(logger, credential.CustomSignedAssertionProviderName ?? "undefined");
return credential;
}
}
}

if (credential.CredentialType == CredentialType.Certificate)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Microsoft.Identity.Web.TokenAcquisitionExtensionOptions!>?
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
Loading
Loading