From 05ab8b74285cebdc4ffef8ffbb43e356615610b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Wed, 17 Jan 2024 15:05:52 +0100 Subject: [PATCH] Update the ClaimsIdentity/ClaimsPrincipal.GetClaim() extension to throw an exception when multiple claims of the same type exist --- .../OpenIddictResources.resx | 3 + .../Primitives/OpenIddictExtensions.cs | 70 +++++++++++++++++-- .../Primitives/OpenIddictExtensionsTests.cs | 34 +++++++++ 3 files changed, 101 insertions(+), 6 deletions(-) diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index c467b571..30ec5114 100644 --- a/src/OpenIddict.Abstractions/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx @@ -1575,6 +1575,9 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId A configuration manager must be attached to the client registration to be able to resolve the server configuration. + + Multiple claims of the same type are present in the identity or principal. + The security token is missing. diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs index 31f90040..033a7bc1 100644 --- a/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs +++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs @@ -1462,10 +1462,13 @@ public static class OpenIddictExtensions } /// - /// Gets the claim value corresponding to the given type. + /// Gets the unique claim value corresponding to the given type. /// /// The identity. /// The type associated with the claim. + /// + /// Multiple claims using the same are present in the identity. + /// /// The claim value. public static string? GetClaim(this ClaimsIdentity identity, string type) { @@ -1479,14 +1482,43 @@ public static class OpenIddictExtensions throw new ArgumentException(SR.GetResourceString(SR.ID0184), nameof(type)); } - return identity.FindFirst(type)?.Value; + var claims = identity.FindAll(type); + if (claims is IList list) + { + return list switch + { + [Claim claim] => claim.Value, + [ ] => null, + [ .. ] => throw new InvalidOperationException(SR.GetResourceString(SR.ID0423)) + }; + } + + else + { + using var enumerator = claims.GetEnumerator(); + while (enumerator.MoveNext()) + { + var claim = enumerator.Current; + if (!enumerator.MoveNext()) + { + return claim.Value; + } + + throw new InvalidOperationException(SR.GetResourceString(SR.ID0423)); + } + + return null; + } } /// - /// Gets the claim value corresponding to the given type. + /// Gets the unique claim value corresponding to the given type. /// /// The principal. /// The type associated with the claim. + /// + /// Multiple claims using the same are present in the principal. + /// /// The claim value. public static string? GetClaim(this ClaimsPrincipal principal, string type) { @@ -1500,11 +1532,37 @@ public static class OpenIddictExtensions throw new ArgumentException(SR.GetResourceString(SR.ID0184), nameof(type)); } - return principal.FindFirst(type)?.Value; + var claims = principal.FindAll(type); + if (claims is IList list) + { + return list switch + { + [Claim claim] => claim.Value, + [ ] => null, + [ .. ] => throw new InvalidOperationException(SR.GetResourceString(SR.ID0423)) + }; + } + + else + { + using var enumerator = claims.GetEnumerator(); + while (enumerator.MoveNext()) + { + var claim = enumerator.Current; + if (!enumerator.MoveNext()) + { + return claim.Value; + } + + throw new InvalidOperationException(SR.GetResourceString(SR.ID0423)); + } + + return null; + } } /// - /// Gets the claim values corresponding to the given type. + /// Gets all the claim values corresponding to the given type. /// /// The claims identity. /// The type associated with the claims. @@ -1525,7 +1583,7 @@ public static class OpenIddictExtensions } /// - /// Gets the claim values corresponding to the given type. + /// Gets all the claim values corresponding to the given type. /// /// The claims principal. /// The type associated with the claims. diff --git a/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs b/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs index 87d5cc08..c5eb9127 100644 --- a/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs +++ b/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs @@ -2412,6 +2412,40 @@ public class OpenIddictExtensionsTests Assert.Equal("principal", exception.ParamName); } + [Fact] + public void ClaimsIdentity_GetClaim_ThrowsAnExceptionForDuplicateClaimType() + { + // Arrange + var identity = new ClaimsIdentity(); + identity.AddClaim(Claims.Name, "Bob le Bricoleur"); + identity.AddClaim(Claims.Name, "Bob le Bricoleur"); + + // Act and assert + var exception = Assert.Throws(() => + { + identity.GetClaim(Claims.Name); + }); + + Assert.Equal(SR.GetResourceString(SR.ID0423), exception.Message); + } + + [Fact] + public void ClaimsPrincipal_GetClaim_ThrowsAnExceptionForDuplicateClaimType() + { + // Arrange + var principal = new ClaimsPrincipal(new ClaimsIdentity()); + principal.AddClaim(Claims.Name, "Bob le Bricoleur"); + principal.AddClaim(Claims.Name, "Bob le Bricoleur"); + + // Act and assert + var exception = Assert.Throws(() => + { + principal.GetClaim(Claims.Name); + }); + + Assert.Equal(SR.GetResourceString(SR.ID0423), exception.Message); + } + [Fact] public void ClaimsIdentity_GetClaim_ReturnsNullForMissingClaims() {