Merge pull request #2328 from DuendeSoftware/pg/cherry-pick-changes

Cherry Pick : Fix nullability issues with ClaimRecord and ClaimsPrincipalRecord (#2323) into BFF 4.0.x
This commit is contained in:
Pieter Germishuys 2026-01-13 09:21:38 +01:00 committed by GitHub
commit 34a222d7ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 113 additions and 18 deletions

View file

@ -3,7 +3,7 @@
using System.Text.Json.Serialization;
namespace Duende.Bff.Blazor.Client;
namespace Duende.Bff.Blazor.Client.Internals;
/// <summary>
/// Serialization friendly claim.
@ -28,13 +28,13 @@ internal class ClaimRecord()
/// The type
/// </summary>
[JsonPropertyName("type")]
public string Type { get; init; } = default!;
public string Type { get; init; } = string.Empty;
/// <summary>
/// The value
/// </summary>
[JsonPropertyName("value")]
public object Value { get; init; } = default!;
public object Value { get; init; } = string.Empty;
/// <summary>
/// The value type

View file

@ -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
/// </summary>
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);

View file

@ -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;
/// <summary>
/// Serialization friendly ClaimsPrincipal
@ -28,5 +26,5 @@ internal class ClaimsPrincipalRecord
/// <summary>
/// The claims
/// </summary>
public ClaimRecord[] Claims { get; init; } = default!;
public ClaimRecord[] Claims { get; init; } = [];
}

View file

@ -3,7 +3,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Security.Claims;
using Duende.Bff.Internal;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;

View file

@ -11,7 +11,7 @@ namespace Duende.Bff.Internal;
internal class ClaimRecord()
{
/// <summary>
///
///
/// </summary>
/// <param name="type"></param>
/// <param name="value"></param>
@ -25,13 +25,13 @@ internal class ClaimRecord()
/// The type
/// </summary>
[JsonPropertyName("type")]
public string Type { get; init; } = default!;
public string Type { get; init; } = string.Empty;
/// <summary>
/// The value
/// </summary>
[JsonPropertyName("value")]
public object Value { get; init; } = default!;
public object Value { get; init; } = string.Empty;
/// <summary>
/// The value type

View file

@ -12,8 +12,13 @@ internal static class ClaimRecordExtensions
/// </summary>
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);

View file

@ -26,5 +26,5 @@ internal class ClaimsPrincipalRecord
/// <summary>
/// The claims
/// </summary>
public ClaimRecord[] Claims { get; init; } = default!;
public ClaimRecord[] Claims { get; init; } = [];
}

View file

@ -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<ClaimsPrincipalRecord>(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<ClaimsPrincipalRecord>(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<ClaimsPrincipalRecord>(serializedClaimsPrincipal);
var principal = record!.ToClaimsPrincipal();
principal.Claims.Single().Value.ShouldBe(string.Empty);
}
}