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);
+ }
+}