diff --git a/bff/src/Bff.Blazor.Client/Internals/ClaimRecord.cs b/bff/src/Bff.Blazor.Client/Internals/ClaimRecord.cs index 6f597bfed..8b9046761 100644 --- a/bff/src/Bff.Blazor.Client/Internals/ClaimRecord.cs +++ b/bff/src/Bff.Blazor.Client/Internals/ClaimRecord.cs @@ -3,7 +3,7 @@ using System.Text.Json.Serialization; -namespace Duende.Bff.Blazor.Client; +namespace Duende.Bff.Blazor.Client.Internals; /// /// Serialization friendly claim. @@ -28,13 +28,13 @@ internal class ClaimRecord() /// The type /// [JsonPropertyName("type")] - public string Type { get; init; } = default!; + public string Type { get; init; } = string.Empty; /// /// The value /// [JsonPropertyName("value")] - public object Value { get; init; } = default!; + public object Value { get; init; } = string.Empty; /// /// The value type diff --git a/bff/src/Bff.Blazor.Client/Internals/ClaimRecordExtensions.cs b/bff/src/Bff.Blazor.Client/Internals/ClaimRecordExtensions.cs index 728df3986..2c5d2fe97 100644 --- a/bff/src/Bff.Blazor.Client/Internals/ClaimRecordExtensions.cs +++ b/bff/src/Bff.Blazor.Client/Internals/ClaimRecordExtensions.cs @@ -2,9 +2,8 @@ // See LICENSE in the project root for license information. using System.Security.Claims; -using Duende.Bff.Blazor.Client; -namespace Duende.Bff.Internal; +namespace Duende.Bff.Blazor.Client.Internals; internal static class ClaimRecordExtensions { @@ -13,8 +12,13 @@ internal static class ClaimRecordExtensions /// public static ClaimsPrincipal ToClaimsPrincipal(this ClaimsPrincipalRecord principal) { - var claims = principal.Claims.Select(x => new Claim(x.Type, x.Value.ToString() ?? string.Empty, x.ValueType ?? ClaimValueTypes.String)) - .ToArray(); + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + var claims = principal.Claims is null + ? [] + // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract + // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract + : principal.Claims.Select(x => new Claim(x.Type ?? string.Empty, x.Value?.ToString() ?? string.Empty, x.ValueType ?? ClaimValueTypes.String)).ToArray(); + var id = new ClaimsIdentity(claims, principal.AuthenticationType, principal.NameClaimType, principal.RoleClaimType); diff --git a/bff/src/Bff.Blazor.Client/Internals/ClaimsPrincipalRecord.cs b/bff/src/Bff.Blazor.Client/Internals/ClaimsPrincipalRecord.cs index e5e4ea355..e24a00be7 100644 --- a/bff/src/Bff.Blazor.Client/Internals/ClaimsPrincipalRecord.cs +++ b/bff/src/Bff.Blazor.Client/Internals/ClaimsPrincipalRecord.cs @@ -1,9 +1,7 @@ // Copyright (c) Duende Software. All rights reserved. // See LICENSE in the project root for license information. -using Duende.Bff.Blazor.Client; - -namespace Duende.Bff.Internal; +namespace Duende.Bff.Blazor.Client.Internals; /// /// Serialization friendly ClaimsPrincipal @@ -28,5 +26,5 @@ internal class ClaimsPrincipalRecord /// /// The claims /// - public ClaimRecord[] Claims { get; init; } = default!; + public ClaimRecord[] Claims { get; init; } = []; } diff --git a/bff/src/Bff.Blazor.Client/Internals/PersistentUserService.cs b/bff/src/Bff.Blazor.Client/Internals/PersistentUserService.cs index 26af42470..870299680 100644 --- a/bff/src/Bff.Blazor.Client/Internals/PersistentUserService.cs +++ b/bff/src/Bff.Blazor.Client/Internals/PersistentUserService.cs @@ -3,7 +3,6 @@ using System.Diagnostics.CodeAnalysis; using System.Security.Claims; -using Duende.Bff.Internal; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.Logging; diff --git a/bff/src/Bff/Internal/ClaimRecord.cs b/bff/src/Bff/Internal/ClaimRecord.cs index 2f9788503..321e12879 100644 --- a/bff/src/Bff/Internal/ClaimRecord.cs +++ b/bff/src/Bff/Internal/ClaimRecord.cs @@ -11,7 +11,7 @@ namespace Duende.Bff.Internal; internal class ClaimRecord() { /// - /// + /// /// /// /// @@ -25,13 +25,13 @@ internal class ClaimRecord() /// The type /// [JsonPropertyName("type")] - public string Type { get; init; } = default!; + public string Type { get; init; } = string.Empty; /// /// The value /// [JsonPropertyName("value")] - public object Value { get; init; } = default!; + public object Value { get; init; } = string.Empty; /// /// The value type diff --git a/bff/src/Bff/Internal/ClaimRecordExtensions.cs b/bff/src/Bff/Internal/ClaimRecordExtensions.cs index cbe959a00..f6feb506c 100644 --- a/bff/src/Bff/Internal/ClaimRecordExtensions.cs +++ b/bff/src/Bff/Internal/ClaimRecordExtensions.cs @@ -12,8 +12,13 @@ internal static class ClaimRecordExtensions /// public static ClaimsPrincipal ToClaimsPrincipal(this ClaimsPrincipalRecord principal) { - var claims = principal.Claims.Select(x => new Claim(x.Type, x.Value.ToString() ?? string.Empty, x.ValueType ?? ClaimValueTypes.String)) - .ToArray(); + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + var claims = principal.Claims is null + ? [] + // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract + // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract + : principal.Claims.Select(x => new Claim(x.Type ?? string.Empty, x.Value?.ToString() ?? string.Empty, x.ValueType ?? ClaimValueTypes.String)).ToArray(); + var id = new ClaimsIdentity(claims, principal.AuthenticationType, principal.NameClaimType, principal.RoleClaimType); diff --git a/bff/src/Bff/Internal/ClaimsPrincipalRecord.cs b/bff/src/Bff/Internal/ClaimsPrincipalRecord.cs index c97d1cb13..6f68693a0 100644 --- a/bff/src/Bff/Internal/ClaimsPrincipalRecord.cs +++ b/bff/src/Bff/Internal/ClaimsPrincipalRecord.cs @@ -26,5 +26,5 @@ internal class ClaimsPrincipalRecord /// /// The claims /// - public ClaimRecord[] Claims { get; init; } = default!; + public ClaimRecord[] Claims { get; init; } = []; } diff --git a/bff/test/Bff.Tests/Internal/ClaimsPrincipalRecordTests.cs b/bff/test/Bff.Tests/Internal/ClaimsPrincipalRecordTests.cs new file mode 100644 index 000000000..3d1916546 --- /dev/null +++ b/bff/test/Bff.Tests/Internal/ClaimsPrincipalRecordTests.cs @@ -0,0 +1,89 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + +using System.Security.Claims; +using System.Text.Json; +using Duende.Bff.Internal; + +namespace Duende.Bff.Tests.Internal; + +public class ClaimsPrincipalRecordTests +{ + [Fact] + public void Can_convert_between_ClaimsPrincipal_and_ClaimsPrincipalRecord() + { + var original = new ClaimsPrincipal(new ClaimsIdentity(new[] + { + new Claim("sub", "123"), + new Claim("name", "Alice"), + new Claim("role", "admin") + }, "TestAuthType", "name", "role")); + + var record = original.ToClaimsPrincipalLite(); + var reconstructed = record.ToClaimsPrincipal(); + + reconstructed.Identity!.AuthenticationType.ShouldBe(original.Identity!.AuthenticationType); + reconstructed.Identity!.Name.ShouldBe(original.Identity!.Name); + + var originalClaims = original.Claims.ToDictionary(c => c.Type, c => c.Value); + var reconstructedClaims = reconstructed.Claims.ToDictionary(c => c.Type, c => c.Value); + + originalClaims.Count.ShouldBe(reconstructedClaims.Count); + foreach (var kvp in originalClaims) + { + reconstructedClaims.ShouldContainKey(kvp.Key); + reconstructedClaims[kvp.Key].ShouldBe(kvp.Value); + } + } + + [Fact] + public void Can_convert_default_ClaimsPrincipalRecord() + { + var original = new ClaimsPrincipalRecord(); + + Should.NotThrow(() => original.ToClaimsPrincipal()); + } + + [Fact] + public void Can_convert_ClaimsPrincipalRecord_with_default_ClaimsRecord() + { + var original = new ClaimsPrincipalRecord + { + Claims = + [ + new ClaimRecord() + ] + }; + + Should.NotThrow(() => original.ToClaimsPrincipal()); + } + + [Fact] + public void ToClaimsPrincipal_handles_null_Claims_from_deserialization() + { + var serializedClaimsPrincipal = """{"Claims": null}"""; + var record = JsonSerializer.Deserialize(serializedClaimsPrincipal); + + Should.NotThrow(() => record!.ToClaimsPrincipal()); + } + + [Fact] + public void ToClaimsPrincipal_handles_null_Type_in_ClaimRecord_from_deserialization() + { + var serializedClaimsPrincipal = """{"Claims": [{"type": null, "value": "test"}]}"""; + var record = JsonSerializer.Deserialize(serializedClaimsPrincipal); + + var principal = record!.ToClaimsPrincipal(); + principal.Claims.Single().Type.ShouldBe(string.Empty); + } + + [Fact] + public void ToClaimsPrincipal_handles_null_Value_in_ClaimRecord_from_deserialization() + { + var serializedClaimsPrincipal = """{"Claims": [{"type": "sub", "value": null}]}"""; + var record = JsonSerializer.Deserialize(serializedClaimsPrincipal); + + var principal = record!.ToClaimsPrincipal(); + principal.Claims.Single().Value.ShouldBe(string.Empty); + } +}