Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
<MicrosoftGraphVersion>4.36.0</MicrosoftGraphVersion>
<MicrosoftGraphBetaVersion>4.57.0-preview</MicrosoftGraphBetaVersion>
<MicrosoftExtensionsHttpVersion>3.1.3</MicrosoftExtensionsHttpVersion>
<MicrosoftIdentityAbstractionsVersion>8.0.0</MicrosoftIdentityAbstractionsVersion>
<MicrosoftIdentityAbstractionsVersion>8.1.0</MicrosoftIdentityAbstractionsVersion>
<!--CVE-2024-43485-->
<SystemTextJsonVersion>8.0.5</SystemTextJsonVersion>
<!--CVE-2023-29331-->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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. ";
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Constructor for DefaultCredentialsLoader when using custom signed assertion provider source loaders.
/// </summary>
/// <param name="logger"></param>
/// <param name="customSignedAssertionProviders">Set of custom signed assertion providers.</param>
public DefaultCredentialsLoader(ILogger<DefaultCredentialsLoader>? logger, IEnumerable<ICustomSignedAssertionProvider> customSignedAssertionProviders) : this(logger)
{
var sourceLoaderDict = new Dictionary<string, ICredentialSourceLoader>();

foreach (var provider in customSignedAssertionProviders)
{
sourceLoaderDict.Add(provider.Name ?? provider.GetType().FullName!, provider);
}

CustomSignedAssertionCredentialSourceLoaders = sourceLoaderDict;
}

/// <summary>
/// Dictionary of custom signed assertion credential source loaders, by name (fully qualified type name).
/// </summary>
public IDictionary<string, ICredentialSourceLoader>? 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;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,20 @@ 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<ILogger, string, string, bool, Exception?> s_customSignedAssertionProviderLoadingFailure =
LoggerMessage.Define<string, string, bool>(
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,
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,8 @@
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!
static Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException.ProviderNameNullOrEmpty() -> Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException!
static Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException.SourceLoadersNullOrEmpty() -> Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException!
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#nullable enable
Microsoft.Identity.Web.DefaultCredentialsLoader.CustomSignedAssertionCredentialSourceLoaders.get -> System.Collections.Generic.IDictionary<string!, Microsoft.Identity.Abstractions.ICredentialSourceLoader!>?
Microsoft.Identity.Web.DefaultCredentialsLoader.DefaultCredentialsLoader(Microsoft.Extensions.Logging.ILogger<Microsoft.Identity.Web.DefaultCredentialsLoader!>? logger, System.Collections.Generic.IEnumerable<Microsoft.Identity.Abstractions.ICustomSignedAssertionProvider!>! customSignedAssertionProviders) -> void
Original file line number Diff line number Diff line change
@@ -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!
Original file line number Diff line number Diff line change
@@ -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!
Original file line number Diff line number Diff line change
@@ -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!
Original file line number Diff line number Diff line change
@@ -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!
1 change: 1 addition & 0 deletions tests/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<MicrosoftApplicationInsightsEventCounterCollectionVersion>2.22.0</MicrosoftApplicationInsightsEventCounterCollectionVersion>
<MicrosoftExtensionsCachingStackExchangeRedisVersion>6.0.12</MicrosoftExtensionsCachingStackExchangeRedisVersion>
<MicrosoftPlaywrightVersion>1.48.0</MicrosoftPlaywrightVersion>
<MoqVersion>4.20.72</MoqVersion>
<StackExchangeRedisVersion>2.2.4</StackExchangeRedisVersion>
<!--CVE-2021-24112-->
<SystemDrawingCommonVersion>5.0.3</SystemDrawingCommonVersion>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// 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 Moq;
using Microsoft.Extensions.Logging;
using Microsoft.Identity.Abstractions;
using Xunit;

namespace Microsoft.Identity.Web.Test
{
public class CustomSignedAssertionProviderTests
{
[Theory]
[MemberData(nameof(CustomSignedAssertionTestData))]
public async Task ProcessCustomSignedAssertionAsync_Tests(
List<ICustomSignedAssertionProvider> providerList,
CredentialDescription credentialDescription,
string? expectedMessage = null)
{
// Arrange
var loggedMessages = new List<string>();
var loggerMock = new Mock<ILogger<DefaultCredentialsLoader>>();
loggerMock.Setup(x => x.IsEnabled(It.IsAny<LogLevel>())).Returns(true);

var loader = new DefaultCredentialsLoader(loggerMock.Object, providerList);

// Act
try
{
await loader.LoadCredentialsIfNeededAsync(credentialDescription, null);

}
catch (Exception ex)
{
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<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains(expectedMessage!)),
It.IsAny<Exception?>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Once);*/
return;
}

// Assert
if (expectedMessage != null)
{
loggerMock.Verify(
x => x.Log(
LogLevel.Error,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains(expectedMessage)),
It.IsAny<Exception?>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Once);
}
else
{
loggerMock.Verify(
x => x.Log(
It.IsAny<LogLevel>(),
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Never);
}
}

public static IEnumerable<object[]> CustomSignedAssertionTestData()
{
// No source loaders
yield return new object[]
{
new List<ICustomSignedAssertionProvider>(),
new CredentialDescription
{
CustomSignedAssertionProviderName = "Provider1",
SourceType = CredentialSource.CustomSignedAssertion,
Skip = false
},
CertificateErrorMessage.CustomProviderSourceLoaderNullOrEmpty
};

// No provider name given
yield return new object[]
{
new List<ICustomSignedAssertionProvider> { new SuccessfulCustomSignedAssertionProvider("Provider2") },
new CredentialDescription
{
CustomSignedAssertionProviderName = null,
SourceType = CredentialSource.CustomSignedAssertion
},
CertificateErrorMessage.CustomProviderNameNullOrEmpty
};

// Given provider name not found
yield return new object[]
{
new List<ICustomSignedAssertionProvider> { new SuccessfulCustomSignedAssertionProvider("NotProvider3") },
new CredentialDescription
{
CustomSignedAssertionProviderName = "Provider3",
SourceType = CredentialSource.CustomSignedAssertion
},
string.Format(CultureInfo.InvariantCulture, CertificateErrorMessage.CustomProviderNotFound, "Provider3")
};

// Happy path (no logging expected)
yield return new object[]
{
new List<ICustomSignedAssertionProvider> { 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<ICustomSignedAssertionProvider> { new FailingCustomSignedAssertionProvider("Provider5") },
new CredentialDescription
{
CustomSignedAssertionProviderName = "Provider5",
SourceType = CredentialSource.CustomSignedAssertion
},
FailingCustomSignedAssertionProvider.ExceptionMessage
};
}
}

// Helper class
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;
}
}

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 :(");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<PackageReference Include="System.Text.Json" Version="$(SystemTextJsonVersion)" />
<PackageReference Include="Microsoft.Identity.Abstractions" Version="$(MicrosoftIdentityAbstractionsVersion)" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
<PackageReference Include="Moq" Version="$(MoqVersion)" />
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonVersion)" />
<PackageReference Include="NSubstitute" Version="$(NSubstituteVersion)" />
<PackageReference Include="NSubstitute.Analyzers.CSharp" Version="$(NSubstituteAnalyzersCSharpVersion)" />
Expand Down
Loading