Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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 @@ -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,
CustomSignedAssertionProviderNotFoundException 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,111 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Identity.Abstractions;

namespace Microsoft.Identity.Web
{
public partial class DefaultCredentialsLoader : ICredentialsLoader
{
/// <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)
{
CustomSignedAssertionProviderNotFoundException providerNotFoundException;

// No source loader(s)
if (CustomSignedAssertionCredentialSourceLoaders == null || !CustomSignedAssertionCredentialSourceLoaders.Any())
{
providerNotFoundException = CustomSignedAssertionProviderNotFoundException.SourceLoadersNullOrEmpty();
}

// No provider name
else if (string.IsNullOrEmpty(credentialDescription.CustomSignedAssertionProviderName))
{
providerNotFoundException = CustomSignedAssertionProviderNotFoundException.ProviderNameNullOrEmpty();
}

// No source loader for provider name
else if (!CustomSignedAssertionCredentialSourceLoaders!.TryGetValue(credentialDescription.CustomSignedAssertionProviderName!, out ICredentialSourceLoader? sourceLoader))
{
providerNotFoundException = CustomSignedAssertionProviderNotFoundException.ProviderNameNotFound(credentialDescription.CustomSignedAssertionProviderName!);
}

// Load the credentials
else
{
await sourceLoader.LoadIfNeededAsync(credentialDescription, parameters);
return;
}

Logger.CustomSignedAssertionProviderLoadingFailure(_logger, credentialDescription, providerNotFoundException);
throw providerNotFoundException;
}
}

internal class CustomSignedAssertionProviderNotFoundException : Exception
{
private const string NameNullOrEmpty = "The name of the custom signed assertion provider is null or empty.";
private const string SourceLoaderNullOrEmpty = "The dictionary of SourceLoaders for custom signed assertion providers is null or empty.";
private const string ProviderNotFound = "The custom signed assertion provider with name '{0}' was not found.";

public CustomSignedAssertionProviderNotFoundException(string message) : base(message)
{
}

/// <summary>
/// Use when the SourceLoader library has entries, but the given name is not found.
/// </summary>
/// <param name="name">Name of custom signed assertion provider</param>
/// <returns>An instance of this exception with a relevant message</returns>
public static CustomSignedAssertionProviderNotFoundException ProviderNameNotFound(string name)
{
return new CustomSignedAssertionProviderNotFoundException(message: string.Format(CultureInfo.InvariantCulture, ProviderNotFound, name));
}

/// <summary>
/// Use when the name of the custom signed assertion provider is null or empty.
/// </summary>
/// <returns>An instance of this exception with a relevant message</returns>
public static CustomSignedAssertionProviderNotFoundException ProviderNameNullOrEmpty()
{
return new CustomSignedAssertionProviderNotFoundException(NameNullOrEmpty);
}

/// <summary>
/// Use when the SourceLoader library is null or empty.
/// </summary>
/// <returns>An instance of this exception with a relevant message</returns>
public static CustomSignedAssertionProviderNotFoundException SourceLoadersNullOrEmpty()
{
return new CustomSignedAssertionProviderNotFoundException(SourceLoaderNullOrEmpty);
}
}
}
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.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!
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Identity.Abstractions;
using Xunit;

namespace Microsoft.Identity.Web.Test
{
public class CustomSignedAssertionProviderTests
{

[Theory]
[MemberData(nameof(CustomSignedAssertionTestData))]
public async Task ProcessCustomSignedAssertionAsync_Tests(DefaultCredentialsLoader loader, CredentialDescription credentialDescription, Exception? expectedException = null)
{
try
{
await loader.LoadCredentialsIfNeededAsync(credentialDescription, null);
}
catch (Exception ex)
{
Assert.Equal(expectedException?.Message, ex.Message);
return;
}

Assert.Null(expectedException);
}

public static IEnumerable<object[]> CustomSignedAssertionTestData()
{
// No source loaders
yield return new object[]
{
new DefaultCredentialsLoader(NullLogger<DefaultCredentialsLoader>.Instance, new List<ICustomSignedAssertionProvider>()),
new CredentialDescription {
CustomSignedAssertionProviderName = "Provider1",
SourceType = CredentialSource.CustomSignedAssertion
},
CustomSignedAssertionProviderNotFoundException.SourceLoadersNullOrEmpty()
};

// No provider name
yield return new object[]
{
new DefaultCredentialsLoader(NullLogger<DefaultCredentialsLoader>.Instance, new List<ICustomSignedAssertionProvider> { new CustomSignedAssertionProvider("Provider1") }),
new CredentialDescription
{
CustomSignedAssertionProviderName = null,
SourceType = CredentialSource.CustomSignedAssertion
},
CustomSignedAssertionProviderNotFoundException.ProviderNameNullOrEmpty()
};

// Provider name not found
yield return new object[]
{
new DefaultCredentialsLoader(NullLogger<DefaultCredentialsLoader>.Instance, new List<ICustomSignedAssertionProvider> { new CustomSignedAssertionProvider("OtherProvider") }),
new CredentialDescription
{
CustomSignedAssertionProviderName = "Provider2",
SourceType = CredentialSource.CustomSignedAssertion
},
CustomSignedAssertionProviderNotFoundException.ProviderNameNotFound("Provider2")
};

// Happy path
yield return new object[]
{
new DefaultCredentialsLoader(NullLogger<DefaultCredentialsLoader>.Instance, new List<ICustomSignedAssertionProvider> { new CustomSignedAssertionProvider("Provider3") }),
new CredentialDescription
{
CustomSignedAssertionProviderName = "Provider3",
SourceType = CredentialSource.CustomSignedAssertion
}
};
}

}

public class CustomSignedAssertionProvider : ICustomSignedAssertionProvider
{
public string Name { get; }

public CredentialSource CredentialSource => CredentialSource.CustomSignedAssertion;

public CustomSignedAssertionProvider(string name)
{
Name = name;
}

public Task LoadIfNeededAsync(CredentialDescription credentialDescription, CredentialSourceLoaderParameters? parameters)
{
return Task.CompletedTask;
}
}
}
Loading