From 5e65b72b9bf0c92dfcfd6fc49fcd41d0553cf0c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Fri, 27 Mar 2026 18:38:35 +0100 Subject: [PATCH] Explicitly attach a claim value type to the mapped WS-Federation claims --- .../OpenIddictClientOwinHandler.cs | 2 +- .../OpenIddictClientWebIntegrationHandlers.cs | 40 +++++++--- .../OpenIddictClientHandlers.cs | 75 ++++++++++++++----- 3 files changed, 86 insertions(+), 31 deletions(-) diff --git a/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandler.cs b/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandler.cs index 0408ab20..52be258f 100644 --- a/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandler.cs +++ b/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandler.cs @@ -167,7 +167,7 @@ public sealed class OpenIddictClientOwinHandler : AuthenticationHandler (string?) context.UserInfoResponse?["email_address"], @@ -1466,13 +1471,18 @@ public static partial class OpenIddictClientWebIntegrationHandlers // Yandex returns the email address as a custom "default_email" node: ProviderTypes.Yandex => (string?) context.UserInfoResponse?["default_email"], - _ => context.MergedPrincipal.GetClaim(ClaimTypes.Email) - }); + _ => null + }; + + if (!string.IsNullOrEmpty(value)) + { + context.MergedPrincipal.AddClaim(ClaimTypes.Email, value, issuer); + } } if (!context.MergedPrincipal.HasClaim(ClaimTypes.Name)) { - context.MergedPrincipal.SetClaim(ClaimTypes.Name, issuer: issuer, value: context.Registration.ProviderType switch + var value = context.Registration.ProviderType switch { // These providers return the username as a custom "username" node: ProviderTypes.ArcGisOnline or ProviderTypes.Dailymotion or ProviderTypes.DeviantArt or @@ -1554,13 +1564,18 @@ public static partial class OpenIddictClientWebIntegrationHandlers // Zoho returns the username as a custom "Display_Name" node: ProviderTypes.Zoho => (string?) context.UserInfoResponse?["Display_Name"], - _ => context.MergedPrincipal.GetClaim(ClaimTypes.Name) - }); + _ => null + }; + + if (!string.IsNullOrEmpty(value)) + { + context.MergedPrincipal.AddClaim(ClaimTypes.Name, value, issuer); + } } if (!context.MergedPrincipal.HasClaim(ClaimTypes.NameIdentifier)) { - context.MergedPrincipal.SetClaim(ClaimTypes.NameIdentifier, issuer: issuer, value: context.Registration.ProviderType switch + var value = context.Registration.ProviderType switch { // These providers return the user identifier as a custom "user_id" node: ProviderTypes.Amazon or ProviderTypes.HubSpot or @@ -1655,8 +1670,13 @@ public static partial class OpenIddictClientWebIntegrationHandlers // Zoho returns the user identifier as a custom "ZUID" node: ProviderTypes.Zoho => (string?) context.UserInfoResponse?["ZUID"], - _ => context.MergedPrincipal.GetClaim(ClaimTypes.NameIdentifier) - }); + _ => null + }; + + if (!string.IsNullOrEmpty(value)) + { + context.MergedPrincipal.AddClaim(ClaimTypes.NameIdentifier, value, issuer); + } } return ValueTask.CompletedTask; diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.cs index 0279f818..feaea8eb 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.cs @@ -10,6 +10,7 @@ using System.Diagnostics; using System.Runtime.InteropServices; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; +using System.Security.Principal; using System.Text; using System.Text.Json; using Microsoft.Extensions.Logging; @@ -4737,38 +4738,48 @@ public static partial class OpenIddictClientHandlers // Note: a similar event handler exists in OpenIddict.Client.WebIntegration to map these claims // from non-standard/provider-specific claim types (see MapCustomWebServicesFederationClaims). + if (context.MergedPrincipal.Identity is not ClaimsIdentity identity) + { + return ValueTask.CompletedTask; + } + var issuer = context.Registration.ClaimsIssuer ?? context.Registration.ProviderName ?? context.Registration.Issuer.AbsoluteUri; - MapClaim(ClaimTypes.Email, Claims.Email); - MapClaim(ClaimTypes.Gender, Claims.Gender); - MapClaim(ClaimTypes.GivenName, Claims.GivenName); - MapClaim(ClaimTypes.Name, Claims.PreferredUsername, Claims.Name); - MapClaim(ClaimTypes.NameIdentifier, Claims.Subject); - MapClaim(ClaimTypes.OtherPhone, Claims.PhoneNumber); - MapClaim(ClaimTypes.Surname, Claims.FamilyName); + MapClaim(ClaimTypes.Email, ClaimValueTypes.String, [Claims.Email]); + MapClaim(ClaimTypes.Gender, ClaimValueTypes.String, [Claims.Gender]); + MapClaim(ClaimTypes.GivenName, ClaimValueTypes.String, [Claims.GivenName]); + MapClaim(ClaimTypes.Name, ClaimValueTypes.String, [Claims.PreferredUsername, Claims.Name]); + MapClaim(ClaimTypes.NameIdentifier, ClaimValueTypes.String, [Claims.Subject]); + MapClaim(ClaimTypes.OtherPhone, ClaimValueTypes.String, [Claims.PhoneNumber]); + MapClaim(ClaimTypes.Surname, ClaimValueTypes.String, [Claims.FamilyName]); // Note: while this claim is not exposed by the BCL ClaimTypes class, it is used by both ASP.NET Identity // for ASP.NET 4.x and the System.Web.WebPages package, that requires it for antiforgery to work correctly. MapClaim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", - Claims.Private.ProviderName); + ClaimValueTypes.String, [Claims.Private.ProviderName]); return ValueTask.CompletedTask; - void MapClaim(string destinationClaimType, string sourceClaimType, string? alternativeSourceClaimType = null) + void MapClaim(string name, string type, ReadOnlySpan names) { - if (context.MergedPrincipal.HasClaim(destinationClaimType)) + // Do not map the claim if the claim is already present in the merged principal (e.g because it was + // returned by the identity provider or because it was manually added from a custom event handler). + if (context.MergedPrincipal.HasClaim(name)) { return; } - var claim = context.MergedPrincipal.GetClaim(sourceClaimType); - if (claim == null && alternativeSourceClaimType != null) + + // Use the first claim that matches one of the provided claim types. + for (var index = 0; index < names.Length; index++) { - claim = context.MergedPrincipal.GetClaim(alternativeSourceClaimType); + if (context.MergedPrincipal.FindFirst(names[index]) is Claim claim) + { + identity.AddClaim(new Claim(name, claim.Value, type, issuer, issuer, identity)); + return; + } } - - context.MergedPrincipal.SetClaim(destinationClaimType, claim, issuer); } } } @@ -8039,20 +8050,44 @@ public static partial class OpenIddictClientHandlers // WS-Federation equivalent, this handler is responsible for mapping the standard OAuth 2.0 introspection nodes // defined by https://datatracker.ietf.org/doc/html/rfc7662#section-2.2 to their WS-Federation equivalent. + if (context.Principal.Identity is not ClaimsIdentity identity) + { + return ValueTask.CompletedTask; + } + var issuer = context.Registration.ClaimsIssuer ?? context.Registration.ProviderName ?? context.Registration.Issuer.AbsoluteUri; - context.Principal - .SetClaim(ClaimTypes.Name, context.Principal.GetClaim(Claims.Username), issuer) - .SetClaim(ClaimTypes.NameIdentifier, context.Principal.GetClaim(Claims.Subject), issuer); + MapClaim(ClaimTypes.Name, ClaimValueTypes.String, [Claims.Username]); + MapClaim(ClaimTypes.NameIdentifier, ClaimValueTypes.String, [Claims.Subject]); // Note: while this claim is not exposed by the BCL ClaimTypes class, it is used by both ASP.NET Identity // for ASP.NET 4.x and the System.Web.WebPages package, that requires it for antiforgery to work correctly. - context.Principal.SetClaim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", - context.Principal.GetClaim(Claims.Private.ProviderName)); + MapClaim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", + ClaimValueTypes.String, [Claims.Private.ProviderName]); return ValueTask.CompletedTask; + + void MapClaim(string name, string type, ReadOnlySpan names) + { + // Do not map the claim if the claim is already present in the merged principal (e.g because it was + // returned by the identity provider or because it was manually added from a custom event handler). + if (context.Principal.HasClaim(name)) + { + return; + } + + // Use the first claim that matches one of the provided claim types. + for (var index = 0; index < names.Length; index++) + { + if (context.Principal.FindFirst(names[index]) is Claim claim) + { + identity.AddClaim(new Claim(name, claim.Value, type, issuer, issuer, identity)); + return; + } + } + } } }