Browse Source

Add an automatic claims mapping feature to the OpenIddict client stack

pull/1851/head
Kévin Chalet 3 years ago
parent
commit
9deb68d433
  1. 50
      sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/AuthenticationController.cs
  2. 46
      sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthenticationController.cs
  3. 59
      sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/AuthenticationController.cs
  4. 54
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthenticationController.cs
  5. 44
      shared/OpenIddict.Extensions/Helpers/OpenIddictHelpers.cs
  6. 34
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs
  7. 29
      src/OpenIddict.Client.Owin/OpenIddictClientOwinHandler.cs
  8. 50
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.cs
  9. 18
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs
  10. 217
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs
  11. 16
      src/OpenIddict.Client/OpenIddictClientBuilder.cs
  12. 5
      src/OpenIddict.Client/OpenIddictClientEvents.cs
  13. 1
      src/OpenIddict.Client/OpenIddictClientExtensions.cs
  14. 17
      src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs
  15. 12
      src/OpenIddict.Client/OpenIddictClientHandlers.Userinfo.cs
  16. 159
      src/OpenIddict.Client/OpenIddictClientHandlers.cs
  17. 12
      src/OpenIddict.Client/OpenIddictClientOptions.cs
  18. 32
      src/OpenIddict.Client/OpenIddictClientService.cs
  19. 7
      src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs

50
sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/AuthenticationController.cs

@ -173,47 +173,15 @@ namespace OpenIddict.Sandbox.AspNet.Client.Controllers
//
// By default, all claims extracted during the authorization dance are available. The claims collection stored
// in the cookie can be filtered out or mapped to different names depending the claim name or its issuer.
var claims = new List<Claim>(result.Identity.Claims
.Select(claim => claim switch
{
// Map the standard "sub" and custom "id" claims to ClaimTypes.NameIdentifier, which is
// the default claim type used by .NET and is required by the antiforgery components.
{ Type: Claims.Subject } or
{ Type: "id", Issuer: "https://github.com/" or "https://twitter.com/" }
=> new Claim(ClaimTypes.NameIdentifier, claim.Value, claim.ValueType, claim.Issuer),
// Map the standard "name" claim to ClaimTypes.Name.
{ Type: Claims.Name }
=> new Claim(ClaimTypes.Name, claim.Value, claim.ValueType, claim.Issuer),
// The antiforgery components require an "identityprovider" claim, which
// is mapped from the authorization server claim returned by OpenIddict.
{ Type: Claims.AuthorizationServer }
=> new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider",
claim.Value, claim.ValueType, claim.Issuer),
_ => claim
})
.Where(claim => claim switch
{
// Preserve the basic claims that are necessary for the application to work correctly.
{
Type: ClaimTypes.NameIdentifier or
ClaimTypes.Name or
"http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider"
} => true,
// Preserve the client registration identifier as a dedicated claim so that the
// associated server configuration can be resolved from the logout endpoint to
// determine whether the authorization server supports client-initiated logouts.
{ Type: Claims.Private.RegistrationId } => true,
// Applications that use multiple client registrations can filter claims based on the issuer.
{ Type: "bio", Issuer: "https://github.com/" } => true,
// Don't preserve the other claims.
_ => false
}));
var claims = result.Identity.Claims.Where(claim => claim.Type is ClaimTypes.NameIdentifier or ClaimTypes.Name
//
// Preserve the registration identifier to be able to resolve it later.
//
or Claims.Private.RegistrationId
//
// The ASP.NET 4.x antiforgery module requires preserving the "identityprovider" claim.
//
or "http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider");
var identity = new ClaimsIdentity(claims,
authenticationType: CookieAuthenticationDefaults.AuthenticationType,

46
sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthenticationController.cs

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
@ -61,42 +60,15 @@ namespace OpenIddict.Sandbox.AspNet.Server.Controllers
//
// By default, all claims extracted during the authorization dance are available. The claims collection stored
// in the cookie can be filtered out or mapped to different names depending the claim name or its issuer.
var claims = new List<Claim>(result.Identity.Claims
.Select(claim => claim switch
{
// Map the standard "sub" and custom "id" claims to ClaimTypes.NameIdentifier, which is
// the default claim type used by .NET and is required by the antiforgery components.
{ Type: Claims.Subject } or
{ Type: "id", Issuer: "https://github.com/" }
=> new Claim(ClaimTypes.NameIdentifier, claim.Value, claim.ValueType, claim.Issuer),
// Map the standard "name" claim to ClaimTypes.Name.
{ Type: Claims.Name }
=> new Claim(ClaimTypes.Name, claim.Value, claim.ValueType, claim.Issuer),
// The antiforgery components require an "identityprovider" claim, which
// is mapped from the authorization server claim returned by OpenIddict.
{ Type: Claims.AuthorizationServer }
=> new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider",
claim.Value, claim.ValueType, claim.Issuer),
_ => claim
})
.Where(claim => claim switch
{
// Preserve the basic claims that are necessary for the application to work correctly.
{
Type: ClaimTypes.NameIdentifier or
ClaimTypes.Name or
"http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider"
} => true,
// Applications that use multiple client registrations can filter claims based on the issuer.
{ Type: "bio", Issuer: "https://github.com/" } => true,
// Don't preserve the other claims.
_ => false
}));
var claims = result.Identity.Claims.Where(claim => claim.Type is ClaimTypes.NameIdentifier or ClaimTypes.Name
//
// Preserve the registration identifier to be able to resolve it later.
//
or Claims.Private.RegistrationId
//
// The ASP.NET 4.x antiforgery module requires preserving the "identityprovider" claim.
//
or "http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider");
// Note: when using external authentication providers with ASP.NET Identity,
// the user identity MUST be added to the external authentication cookie scheme.

59
sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/AuthenticationController.cs

@ -2,6 +2,7 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Abstractions;
using OpenIddict.Client;
using OpenIddict.Client.AspNetCore;
using static OpenIddict.Abstractions.OpenIddictConstants;
@ -159,44 +160,16 @@ public class AuthenticationController : Controller
// Build an identity based on the external claims and that will be used to create the authentication cookie.
//
// By default, all claims extracted during the authorization dance are available. The claims collection stored
// in the cookie can be filtered out or mapped to different names depending the claim name or its issuer.
var claims = new List<Claim>(result.Principal.Claims
.Select(claim => claim switch
{
// Map the standard "sub" and custom "id" claims to ClaimTypes.NameIdentifier, which is
// the default claim type used by .NET and is required by the antiforgery components.
{ Type: Claims.Subject } or
{ Type: "id", Issuer: "https://github.com/" or "https://twitter.com/" }
=> new Claim(ClaimTypes.NameIdentifier, claim.Value, claim.ValueType, claim.Issuer),
// Map the standard "name" claim to ClaimTypes.Name.
{ Type: Claims.Name }
=> new Claim(ClaimTypes.Name, claim.Value, claim.ValueType, claim.Issuer),
_ => claim
})
.Where(claim => claim switch
{
// Preserve the basic claims that are necessary for the application to work correctly.
{ Type: ClaimTypes.NameIdentifier or ClaimTypes.Name } => true,
// Preserve the client registration identifier as a dedicated claim so that the
// associated server configuration can be resolved from the logout endpoint to
// determine whether the authorization server supports client-initiated logouts.
{ Type: Claims.Private.RegistrationId } => true,
// Applications that use multiple client registrations can filter claims based on the issuer.
{ Type: "bio", Issuer: "https://github.com/" } => true,
// Don't preserve the other claims.
_ => false
}));
var identity = new ClaimsIdentity(claims,
authenticationType: CookieAuthenticationDefaults.AuthenticationScheme,
nameType: ClaimTypes.Name,
roleType: ClaimTypes.Role);
// By default, OpenIddict will automatically try to map the email/name and name identifier claims from
// their standard OpenID Connect or provider-specific equivalent, if available. If needed, additional
// claims can be resolved from the external identity and copied to the final authentication cookie.
var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme)
.SetClaim(ClaimTypes.Email, result.Principal.GetClaim(ClaimTypes.Email))
.SetClaim(ClaimTypes.Name, result.Principal.GetClaim(ClaimTypes.Name))
.SetClaim(ClaimTypes.NameIdentifier, result.Principal.GetClaim(ClaimTypes.NameIdentifier));
// Preserve the registration identifier to be able to resolve it later.
identity.SetClaim(Claims.Private.RegistrationId, result.Principal.GetClaim(Claims.Private.RegistrationId));
// Build the authentication properties based on the properties that were added when the challenge was triggered.
var properties = new AuthenticationProperties(result.Properties.Items)
@ -205,6 +178,7 @@ public class AuthenticationController : Controller
};
// If needed, the tokens returned by the authorization server can be stored in the authentication cookie.
//
// To make cookies less heavy, tokens that are not used are filtered out before creating the cookie.
properties.StoreTokens(result.Properties.GetTokens().Where(token => token switch
{
@ -219,9 +193,12 @@ public class AuthenticationController : Controller
_ => false
}));
// Ask the cookie authentication handler to return a new cookie and redirect
// the user agent to the return URL stored in the authentication properties.
return SignIn(new ClaimsPrincipal(identity), properties, CookieAuthenticationDefaults.AuthenticationScheme);
// Ask the default sign-in handler to return a new cookie and redirect the
// user agent to the return URL stored in the authentication properties.
//
// For scenarios where the default sign-in handler configured in the ASP.NET Core
// authentication options shouldn't be used, a specific scheme can be specified here.
return SignIn(new ClaimsPrincipal(identity), properties);
}
// Note: this controller uses the same callback action for all providers

54
sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthenticationController.cs

@ -1,7 +1,9 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Abstractions;
using OpenIddict.Client.AspNetCore;
using static OpenIddict.Abstractions.OpenIddictConstants;
@ -52,41 +54,16 @@ public class AuthenticationController : Controller
// Build an identity based on the external claims and that will be used to create the authentication cookie.
//
// By default, all claims extracted during the authorization dance are available. The claims collection stored
// in the cookie can be filtered out or mapped to different names depending the claim name or its issuer.
var claims = new List<Claim>(result.Principal.Claims
.Select(claim => claim switch
{
// Map the standard "sub" and custom "id" claims to ClaimTypes.NameIdentifier, which is
// the default claim type used by .NET and is required by the antiforgery components.
{ Type: Claims.Subject } or
{ Type: "id", Issuer: "https://github.com/" or "https://twitter.com/" }
=> new Claim(ClaimTypes.NameIdentifier, claim.Value, claim.ValueType, claim.Issuer),
// Map the standard "name" claim to ClaimTypes.Name.
{ Type: Claims.Name }
=> new Claim(ClaimTypes.Name, claim.Value, claim.ValueType, claim.Issuer),
_ => claim
})
.Where(claim => claim switch
{
// Preserve the basic claims that are necessary for the application to work correctly.
{ Type: ClaimTypes.NameIdentifier or ClaimTypes.Name } => true,
// By default, OpenIddict will automatically try to map the email/name and name identifier claims from
// their standard OpenID Connect or provider-specific equivalent, if available. If needed, additional
// claims can be resolved from the external identity and copied to the final authentication cookie.
var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme)
.SetClaim(ClaimTypes.Email, result.Principal.GetClaim(ClaimTypes.Email))
.SetClaim(ClaimTypes.Name, result.Principal.GetClaim(ClaimTypes.Name))
.SetClaim(ClaimTypes.NameIdentifier, result.Principal.GetClaim(ClaimTypes.NameIdentifier));
// Applications that use multiple client registrations can filter claims based on the issuer.
{ Type: "bio", Issuer: "https://github.com/" } => true,
// Don't preserve the other claims.
_ => false
}));
// Note: when using external authentication providers with ASP.NET Core Identity,
// the user identity MUST be added to the external authentication cookie scheme.
var identity = new ClaimsIdentity(claims,
authenticationType: IdentityConstants.ExternalScheme,
nameType: ClaimTypes.NameIdentifier,
roleType: ClaimTypes.Role);
// Preserve the registration identifier to be able to resolve it later.
identity.SetClaim(Claims.Private.RegistrationId, result.Principal.GetClaim(Claims.Private.RegistrationId));
// Build the authentication properties based on the properties that were added when the challenge was triggered.
var properties = new AuthenticationProperties(result.Properties.Items)
@ -108,8 +85,11 @@ public class AuthenticationController : Controller
_ => false
}));
// Ask the cookie authentication handler to return a new cookie and redirect
// the user agent to the return URL stored in the authentication properties.
return SignIn(new ClaimsPrincipal(identity), properties, IdentityConstants.ExternalScheme);
// Ask the default sign-in handler to return a new cookie and redirect the
// user agent to the return URL stored in the authentication properties.
//
// For scenarios where the default sign-in handler configured in the ASP.NET Core
// authentication options shouldn't be used, a specific scheme can be specified here.
return SignIn(new ClaimsPrincipal(identity), properties);
}
}

44
shared/OpenIddict.Extensions/Helpers/OpenIddictHelpers.cs

@ -376,50 +376,6 @@ internal static class OpenIddictHelpers
.ToDictionary(pair => pair.Key!, pair => new StringValues(pair.Select(parts => parts.Value).ToArray()));
}
/// <summary>
/// Creates a merged principal based on the specified principals.
/// </summary>
/// <param name="principals">The collection of principals to merge.</param>
/// <returns>The merged principal.</returns>
public static ClaimsPrincipal CreateMergedPrincipal(params ClaimsPrincipal?[] principals)
{
// Note: components like the client handler can be used as a pure OAuth 2.0 stack for
// delegation scenarios where the identity of the user is not needed. In this case,
// since no principal can be resolved from a token or a userinfo response to construct
// a user identity, a fake one containing an "unauthenticated" identity (i.e with its
// AuthenticationType property deliberately left to null) is used to allow the host
// to return a "successful" authentication result for these delegation-only scenarios.
if (!Array.Exists(principals, static principal => principal?.Identity is ClaimsIdentity { IsAuthenticated: true }))
{
return new ClaimsPrincipal(new ClaimsIdentity());
}
// Create a new composite identity containing the claims of all the principals.
var identity = new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType);
foreach (var principal in principals)
{
// Note: the principal may be null if no value was extracted from the corresponding token.
if (principal is null)
{
continue;
}
foreach (var claim in principal.Claims)
{
// If a claim with the same type and the same value already exist, skip it.
if (identity.HasClaim(claim.Type, claim.Value))
{
continue;
}
identity.AddClaim(claim);
}
}
return new ClaimsPrincipal(identity);
}
#if SUPPORTS_ECDSA
/// <summary>
/// Creates a new <see cref="ECDsa"/> key.

34
src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs

@ -5,14 +5,12 @@
*/
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenIddict.Extensions;
using static OpenIddict.Client.AspNetCore.OpenIddictClientAspNetCoreConstants;
using Properties = OpenIddict.Client.AspNetCore.OpenIddictClientAspNetCoreConstants.Properties;
@ -149,41 +147,15 @@ public sealed class OpenIddictClientAspNetCoreHandler : AuthenticationHandler<Op
else
{
Debug.Assert(context.Registration.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013));
// A single main claims-based principal instance can be attached to an authentication ticket.
// To return the most appropriate one, the principal is selected based on the endpoint type.
// Independently of the selected main principal, all principals resolved from validated tokens
// are attached to the authentication properties bag so they can be accessed from user code.
var principal = context.EndpointType switch
{
// Create a composite principal containing claims resolved from the frontchannel
// and backchannel identity tokens and the userinfo token principal, if available.
OpenIddictClientEndpointType.Redirection => OpenIddictHelpers.CreateMergedPrincipal(
context.FrontchannelIdentityTokenPrincipal,
context.BackchannelIdentityTokenPrincipal,
context.UserinfoTokenPrincipal),
OpenIddictClientEndpointType.PostLogoutRedirection => context.StateTokenPrincipal,
_ => null
};
if (principal is null)
if (context.MergedPrincipal is not ClaimsPrincipal principal)
{
return AuthenticateResult.NoResult();
}
// Attach the registration identifier and identity of the authorization server to the returned principal to allow
// resolving it even if no other claim was added (e.g if no id_token was returned/no userinfo endpoint is available).
principal.SetClaim(Claims.AuthorizationServer, context.Registration.Issuer.AbsoluteUri)
.SetClaim(Claims.Private.RegistrationId, context.Registration.RegistrationId)
.SetClaim(Claims.Private.ProviderName, context.Registration.ProviderName);
// Restore or create a new authentication properties collection and populate it.
var properties = CreateProperties(context.StateTokenPrincipal);
properties.ExpiresUtc = principal.GetExpirationDate();
properties.IssuedUtc = principal.GetCreationDate();
properties.ExpiresUtc = context.StateTokenPrincipal?.GetExpirationDate();
properties.IssuedUtc = context.StateTokenPrincipal?.GetCreationDate();
// Restore the target link URI that was stored in the state
// token when the challenge operation started, if available.

29
src/OpenIddict.Client.Owin/OpenIddictClientOwinHandler.cs

@ -170,38 +170,15 @@ public sealed class OpenIddictClientOwinHandler : AuthenticationHandler<OpenIddi
else
{
Debug.Assert(context.Registration.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013));
// A single main claims-based principal instance can be attached to an authentication ticket.
var principal = context.EndpointType switch
{
// Create a composite principal containing claims resolved from the frontchannel
// and backchannel identity tokens and the userinfo token principal, if available.
OpenIddictClientEndpointType.Redirection => OpenIddictHelpers.CreateMergedPrincipal(
context.FrontchannelIdentityTokenPrincipal,
context.BackchannelIdentityTokenPrincipal,
context.UserinfoTokenPrincipal),
OpenIddictClientEndpointType.PostLogoutRedirection => context.StateTokenPrincipal,
_ => null
};
if (principal is null)
if (context.MergedPrincipal is not ClaimsPrincipal principal)
{
return null;
}
// Attach the registration identifier and identity of the authorization server to the returned principal to allow
// resolving it even if no other claim was added (e.g if no id_token was returned/no userinfo endpoint is available).
principal.SetClaim(Claims.AuthorizationServer, context.Registration.Issuer.AbsoluteUri)
.SetClaim(Claims.Private.RegistrationId, context.Registration.RegistrationId)
.SetClaim(Claims.Private.ProviderName, context.Registration.ProviderName);
// Restore or create a new authentication properties collection and populate it.
var properties = CreateProperties(context.StateTokenPrincipal);
properties.ExpiresUtc = principal.GetExpirationDate();
properties.IssuedUtc = principal.GetCreationDate();
properties.ExpiresUtc = context.StateTokenPrincipal?.GetExpirationDate();
properties.IssuedUtc = context.StateTokenPrincipal?.GetCreationDate();
// Restore the target link URI that was stored in the state
// token when the challenge operation started, if available.

50
src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.cs

@ -45,11 +45,14 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
* Authentication processing:
*/
WaitMarshalledAuthentication.Descriptor,
RestoreStateTokenFromMarshalledAuthentication.Descriptor,
RestoreStateTokenPrincipalFromMarshalledAuthentication.Descriptor,
RestoreClientRegistrationFromMarshalledContext.Descriptor,
RedirectProtocolActivation.Descriptor,
ResolveRequestForgeryProtection.Descriptor,
RestoreFrontchannelTokensFromMarshalledAuthentication.Descriptor,
RestoreFrontchannelIdentityTokenPrincipalFromMarshalledAuthentication.Descriptor,
RestoreFrontchannelAccessTokenPrincipalFromMarshalledAuthentication.Descriptor,
@ -60,6 +63,8 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
RestoreBackchannelAccessTokenPrincipalFromMarshalledAuthentication.Descriptor,
RestoreRefreshTokenPrincipalFromMarshalledAuthentication.Descriptor,
RestoreUserinfoDetailsFromMarshalledAuthentication.Descriptor,
RestoreMergedPrincipalFromMarshalledAuthentication.Descriptor,
CompleteAuthenticationOperation.Descriptor,
UntrackMarshalledAuthenticationOperation.Descriptor,
@ -1353,6 +1358,51 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
}
}
/// <summary>
/// Contains the logic responsible for restoring the merged principal from the marshalled authentication context, if applicable.
/// </summary>
public sealed class RestoreMergedPrincipalFromMarshalledAuthentication : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
private readonly OpenIddictClientSystemIntegrationMarshal _marshal;
public RestoreMergedPrincipalFromMarshalledAuthentication(OpenIddictClientSystemIntegrationMarshal marshal)
=> _marshal = marshal ?? throw new ArgumentNullException(nameof(marshal));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireAuthenticationNonce>()
.UseSingletonHandler<RestoreMergedPrincipalFromMarshalledAuthentication>()
.SetOrder(PopulateMergedPrincipal.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(!string.IsNullOrEmpty(context.Nonce), SR.GetResourceString(SR.ID4019));
context.MergedPrincipal = context.EndpointType switch
{
// When the authentication context is marshalled, restore the merged principal from the other instance.
OpenIddictClientEndpointType.Unknown when _marshal.TryGetResult(context.Nonce, out var notification)
=> notification.MergedPrincipal,
// Otherwise, don't alter the current context.
_ => context.MergedPrincipal
};
return default;
}
}
/// <summary>
/// Contains the logic responsible for informing the authentication service the operation is complete.
/// </summary>

18
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs

@ -384,14 +384,11 @@ public static partial class OpenIddictClientWebIntegrationHandlers
["accounts"] = context.Response["accounts"]
},
// Kroger and Twitter return a nested "data" object.
ProviderTypes.Kroger or ProviderTypes.Twitter => new(context.Response["data"]?.GetNamedParameters() ??
// Kroger, Twitter and Patreon return a nested "data" object.
ProviderTypes.Kroger or ProviderTypes.Patreon or ProviderTypes.Twitter
=> new(context.Response["data"]?.GetNamedParameters() ??
throw new InvalidOperationException(SR.FormatID0334("data"))),
// Patreon returns a nested "attributes" object that is itself nested in a "data" node.
ProviderTypes.Patreon => new(context.Response["data"]?["attributes"]?.GetNamedParameters() ??
throw new InvalidOperationException(SR.FormatID0334("data/attributes"))),
// ServiceChannel returns a nested "UserProfile" object.
ProviderTypes.ServiceChannel => new(context.Response["UserProfile"]?.GetNamedParameters() ??
throw new InvalidOperationException(SR.FormatID0334("UserProfile"))),
@ -400,15 +397,6 @@ public static partial class OpenIddictClientWebIntegrationHandlers
ProviderTypes.StackExchange => new(context.Response["items"]?[0]?.GetNamedParameters() ??
throw new InvalidOperationException(SR.FormatID0334("items/0"))),
// Streamlabs splits the user data into multiple service-specific nodes (e.g "twitch"/"facebook").
//
// To make claims easier to use, the parameters are flattened and prefixed with the service name.
ProviderTypes.Streamlabs => new(
from parameter in context.Response.GetParameters()
from node in parameter.Value.GetNamedParameters()
let name = $"{parameter.Key}_{node.Key}"
select new KeyValuePair<string, OpenIddictParameter>(name, node.Value)),
// SubscribeStar returns a nested "user" object that is itself nested in a GraphQL "data" node.
ProviderTypes.SubscribeStar => new(context.Response["data"]?["user"]?.GetNamedParameters() ??
throw new InvalidOperationException(SR.FormatID0334("data/user"))),

217
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs

@ -36,6 +36,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
DisableUserinfoValidation.Descriptor,
AttachAdditionalUserinfoRequestParameters.Descriptor,
PopulateUserinfoTokenPrincipalFromTokenResponse.Descriptor,
MapCustomWebServicesFederationClaims.Descriptor,
/*
* Challenge processing:
@ -867,6 +868,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Registration.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013));
Debug.Assert(context.TokenResponse is not null, SR.GetResourceString(SR.ID4007));
// Don't overwrite the userinfo token principal if one was already set.
@ -912,8 +914,6 @@ public static partial class OpenIddictClientWebIntegrationHandlers
context.Registration.TokenValidationParameters.NameClaimType,
context.Registration.TokenValidationParameters.RoleClaimType);
var issuer = context.Configuration.Issuer!.AbsoluteUri;
foreach (var parameter in parameters)
{
// Note: in the typical case, the response parameters should be deserialized from a
@ -928,11 +928,11 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// Top-level claims represented as arrays are split and mapped to multiple CLR claims
// to match the logic implemented by IdentityModel for JWT token deserialization.
case { ValueKind: JsonValueKind.Array } value:
identity.AddClaims(parameter.Key, value, issuer);
identity.AddClaims(parameter.Key, value, context.Registration.Issuer.AbsoluteUri);
break;
case { ValueKind: _ } value:
identity.AddClaim(parameter.Key, value, issuer);
identity.AddClaim(parameter.Key, value, context.Registration.Issuer.AbsoluteUri);
break;
}
}
@ -943,6 +943,215 @@ public static partial class OpenIddictClientWebIntegrationHandlers
}
}
/// <summary>
/// Contains the logic responsible for mapping select custom claims to
/// their WS-Federation equivalent for the providers that require it.
/// </summary>
public sealed class MapCustomWebServicesFederationClaims : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireWebServicesFederationClaimMappingEnabled>()
.UseSingletonHandler<MapCustomWebServicesFederationClaims>()
.SetOrder(MapStandardWebServicesFederationClaims.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Registration.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013));
// As an OpenID Connect framework, the OpenIddict client mostly uses the claim set defined by the OpenID
// Connect core specification (https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims).
// While these claims can be easily accessed using their standard OIDC name, many components still use
// the Web Services Federation claims exposed by the BCL ClaimTypes class, sometimes without allowing
// to use different claim types (e.g ASP.NET Core Identity hardcodes ClaimTypes.NameIdentifier in a few
// places, like the GetUserId() extension). To reduce the difficulty of using the OpenIddict client with
// these components relying on WS-Federation-style claims, these claims are mapped from the custom,
// provider-specific parameters (either from the userinfo response or from the token rensponse).
//
// Note: a similar event handler exists in OpenIddict.Client to map these claims from
// the standard OpenID Connect claim types (see MapStandardWebServicesFederationClaims).
var issuer = context.Registration.Issuer.AbsoluteUri;
context.MergedPrincipal.SetClaim(ClaimTypes.Email, issuer: issuer, value: context.Registration.ProviderType switch
{
// Basecamp returns the email address as a custom "email_address" node:
ProviderTypes.Basecamp => (string?) context.UserinfoResponse?["email_address"],
// HubSpot returns the email address as a custom "user" node:
ProviderTypes.HubSpot => (string?) context.UserinfoResponse?["user"],
// Mailchimp returns the email address as a custom "login/login_email" node:
ProviderTypes.Mailchimp => (string?) context.UserinfoResponse?["login"]?["login_email"],
// Notion returns the email address as a custom "bot/owner/user/person/email" node
// but requires a special capability to access this node, that may not be present:
ProviderTypes.Notion => (string?) context.UserinfoResponse?["bot"]?["owner"]?["user"]?["person"]?["email"],
// Patreon returns the email address as a custom "attributes/email" node:
ProviderTypes.Patreon => (string?) context.UserinfoResponse?["attributes"]?["email"],
// ServiceChannel returns the email address as a custom "Email" node:
ProviderTypes.ServiceChannel => (string?) context.UserinfoResponse?["Email"],
// Shopify returns the email address as a custom "associated_user/email" node in token responses:
ProviderTypes.Shopify => (string?) context.TokenResponse?["associated_user"]?["email"],
_ => context.MergedPrincipal.GetClaim(ClaimTypes.Email)
});
context.MergedPrincipal.SetClaim(ClaimTypes.Name, issuer: issuer, value: context.Registration.ProviderType switch
{
// These providers return the username as a custom "username" node:
ProviderTypes.ArcGisOnline or ProviderTypes.Discord or ProviderTypes.DeviantArt or
ProviderTypes.Lichess or ProviderTypes.Mixcloud or ProviderTypes.Trakt or
ProviderTypes.WordPress
=> (string?) context.UserinfoResponse?["username"],
// Basecamp and Harvest don't return a username so one is created using the "first_name" and "last_name" nodes:
ProviderTypes.Basecamp or ProviderTypes.Harvest
when context.UserinfoResponse?.HasParameter("first_name") is true &&
context.UserinfoResponse?.HasParameter("last_name") is true
=> $"{(string?) context.UserinfoResponse?["first_name"]} {(string?) context.UserinfoResponse?["last_name"]}",
// These providers return the username as a custom "name" node:
ProviderTypes.Deezer or ProviderTypes.Facebook or ProviderTypes.Reddit or
ProviderTypes.SubscribeStar or ProviderTypes.Vimeo
=> (string?) context.UserinfoResponse?["name"],
// FitBit returns the username as a custom "displayName" node:
ProviderTypes.Fitbit => (string?) context.UserinfoResponse?["displayName"],
// HubSpot returns the username as a custom "user" node:
ProviderTypes.HubSpot => (string?) context.UserinfoResponse?["user"],
// Mailchimp returns the username as a custom "accountname" node:
ProviderTypes.Mailchimp => (string?) context.UserinfoResponse?["accountname"],
// Notion returns the username as a custom "bot/owner/user/name" node but
// requires a special capability to access this node, that may not be present:
ProviderTypes.Notion => (string?) context.UserinfoResponse?["bot"]?["owner"]?["user"]?["name"],
// Patreon doesn't return a username and require using the complete user name as the username:
ProviderTypes.Patreon => (string?) context.UserinfoResponse?["attributes"]?["full_name"],
// ServiceChannel returns the username as a custom "UserName" node:
ProviderTypes.ServiceChannel => (string?) context.UserinfoResponse?["UserName"],
// Shopify doesn't return a username so one is created using the "first_name" and "last_name" nodes:
ProviderTypes.Shopify
when context.TokenResponse?["associated_user"]?["first_name"] is not null &&
context.TokenResponse?["associated_user"]?["last_name"] is not null
=> $"{(string?) context.TokenResponse?["associated_user"]?["first_name"]} {(string?) context.TokenResponse?["associated_user"]?["last_name"]}",
// Smartsheet doesn't return a username so one is created using the "firstName" and "lastName" nodes:
ProviderTypes.Smartsheet
when context.UserinfoResponse?.HasParameter("firstName") is true &&
context.UserinfoResponse?.HasParameter("lastName") is true
=> $"{(string?) context.UserinfoResponse?["firstName"]} {(string?) context.UserinfoResponse?["lastName"]}",
// Spotify and StackExchange return the username as a custom "display_name" node:
ProviderTypes.Spotify or ProviderTypes.StackExchange
=> (string?) context.UserinfoResponse?["display_name"],
// Strava returns the username as a custom "athlete/username" node in token responses:
ProviderTypes.Strava => (string?) context.TokenResponse?["athlete"]?["username"],
// Streamlabs returns the username as a custom "streamlabs/display_name" node:
ProviderTypes.Streamlabs => (string?) context.UserinfoResponse?["streamlabs"]?["display_name"],
// Trovo returns the username as a custom "userName" node:
ProviderTypes.Trovo => (string?) context.UserinfoResponse?["userName"],
// Tumblr returns the username as a custom "name" node:
ProviderTypes.Tumblr => (string?) context.UserinfoResponse?["name"],
_ => context.MergedPrincipal.GetClaim(ClaimTypes.Name)
});
context.MergedPrincipal.SetClaim(ClaimTypes.NameIdentifier, issuer: issuer, value: context.Registration.ProviderType switch
{
// ArcGIS and Trakt don't return a user identifier and require using the username as the identifier:
ProviderTypes.ArcGisOnline or ProviderTypes.Trakt
=> (string?) context.UserinfoResponse?["username"],
// These providers return the user identifier as a custom "id" node:
ProviderTypes.Basecamp or ProviderTypes.Deezer or ProviderTypes.Discord or
ProviderTypes.Facebook or ProviderTypes.GitHub or ProviderTypes.Harvest or
ProviderTypes.Kroger or ProviderTypes.Lichess or ProviderTypes.Twitter or
ProviderTypes.Patreon or ProviderTypes.Reddit or ProviderTypes.Smartsheet or
ProviderTypes.Spotify or ProviderTypes.SubscribeStar
=> (string?) context.UserinfoResponse?["id"],
// Bitbucket returns the user identifier as a custom "uuid" node:
ProviderTypes.Bitbucket => (string?) context.UserinfoResponse?["uuid"],
// DeviantArt returns the user identifier as a custom "userid" node:
ProviderTypes.DeviantArt => (string?) context.UserinfoResponse?["userid"],
// Fitbit returns the user identifier as a custom "encodedId" node:
ProviderTypes.Fitbit => (string?) context.UserinfoResponse?["encodedId"],
// HubSpot and StackExchange return the user identifier as a custom "user_id" node:
ProviderTypes.HubSpot or ProviderTypes.StackExchange
=> (string?) context.UserinfoResponse?["user_id"],
// Mailchimp returns the user identifier as a custom "login/login_id" node:
ProviderTypes.Mailchimp => (string?) context.UserinfoResponse?["login"]?["login_id"],
// Mixcloud returns the user identifier as a custom "key" node:
ProviderTypes.Mixcloud => (string?) context.UserinfoResponse?["key"],
// Notion returns the user identifier as a custom "bot/owner/user/id" node but
// requires a special capability to access this node, that may not be present:
ProviderTypes.Notion => (string?) context.UserinfoResponse?["bot"]?["owner"]?["user"]?["id"],
// ServiceChannel returns the user identifier as a custom "UserId" node:
ProviderTypes.ServiceChannel => (string?) context.UserinfoResponse?["UserId"],
// Shopify returns the user identifier as a custom "associated_user/id" node in token responses:
ProviderTypes.Shopify => (string?) context.TokenResponse?["associated_user"]?["id"],
// Strava returns the user identifier as a custom "athlete/id" node in token responses:
ProviderTypes.Strava => (string?) context.TokenResponse?["athlete"]?["id"],
// Stripe returns the user identifier as a custom "stripe_user_id" node in token responses:
ProviderTypes.StripeConnect => (string?) context.TokenResponse?["stripe_user_id"],
// Streamlabs returns the user identifier as a custom "streamlabs/id" node:
ProviderTypes.Streamlabs => (string?) context.UserinfoResponse?["streamlabs"]?["id"],
// Trovo returns the user identifier as a custom "userId" node:
ProviderTypes.Trovo => (string?) context.UserinfoResponse?["userId"],
// Tumblr doesn't return a user identifier and requires using the username as the identifier:
ProviderTypes.Tumblr => (string?) context.UserinfoResponse?["name"],
// Vimeo returns the user identifier as a custom "uri" node, prefixed with "/users/":
ProviderTypes.Vimeo => (string?) context.UserinfoResponse?["uri"] is string uri &&
uri.StartsWith("/users/", StringComparison.Ordinal) ? uri["/users/".Length..] : null,
// WordPress returns the user identifier as a custom "ID" node:
ProviderTypes.WordPress => (string?) context.UserinfoResponse?["ID"],
_ => context.MergedPrincipal.GetClaim(ClaimTypes.NameIdentifier)
});
return default;
}
}
/// <summary>
/// Contains the logic responsible for validating the user-defined authentication properties.
/// </summary>

16
src/OpenIddict.Client/OpenIddictClientBuilder.cs

@ -9,6 +9,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.DependencyInjection.Extensions;
@ -888,6 +889,21 @@ public sealed class OpenIddictClientBuilder
public OpenIddictClientBuilder DisableTokenStorage()
=> Configure(options => options.DisableTokenStorage = true);
/// <summary>
/// Disables automatic claim mapping so that the merged principal returned by
/// OpenIddict after a successful authentication doesn't contain any WS-Federation
/// claims - exposed by the <see cref="ClaimTypes"/> class - mapped from their
/// OpenID Connect/JSON Web Token or provider-specific equivalent.
/// </summary>
/// <remarks>
/// Note: OpenID Connect/JSON Web Token or provider-specific claims that are mapped
/// to their WS-Federation equivalent are never removed from the merged principal
/// when automatic claim mapping is enabled.
/// </remarks>
/// <returns>The <see cref="OpenIddictClientBuilder"/> instance.</returns>
public OpenIddictClientBuilder DisableWebServicesFederationClaimMapping()
=> Configure(options => options.DisableWebServicesFederationClaimMapping = true);
/// <summary>
/// Enables authorization code flow support. For more information
/// about this specific OAuth 2.0/OpenID Connect flow, visit

5
src/OpenIddict.Client/OpenIddictClientEvents.cs

@ -763,6 +763,11 @@ public static partial class OpenIddictClientEvents
/// </summary>
public ClaimsPrincipal? FrontchannelIdentityTokenPrincipal { get; set; }
/// <summary>
/// Gets or sets the merged principal containing the claims of the other principals.
/// </summary>
public ClaimsPrincipal MergedPrincipal { get; set; } = new ClaimsPrincipal(new ClaimsIdentity());
/// <summary>
/// Gets or sets the principal extracted from the refresh token, if applicable.
/// </summary>

1
src/OpenIddict.Client/OpenIddictClientExtensions.cs

@ -67,6 +67,7 @@ public static class OpenIddictClientExtensions
builder.Services.TryAddSingleton<RequireUserinfoTokenExtracted>();
builder.Services.TryAddSingleton<RequireUserinfoTokenPrincipal>();
builder.Services.TryAddSingleton<RequireUserinfoValidationEnabled>();
builder.Services.TryAddSingleton<RequireWebServicesFederationClaimMappingEnabled>();
// Register the built-in client event handlers used by the OpenIddict client components.
// Note: the order used here is not important, as the actual order is set in the options.

17
src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs

@ -544,4 +544,21 @@ public static class OpenIddictClientHandlerFilters
return new(!context.DisableUserinfoValidation);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if the WS-Federation claim mapping feature was disabled.
/// </summary>
public sealed class RequireWebServicesFederationClaimMappingEnabled : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(!context.Options.DisableWebServicesFederationClaimMapping);
}
}
}

12
src/OpenIddict.Client/OpenIddictClientHandlers.Userinfo.cs

@ -5,6 +5,7 @@
*/
using System.Collections.Immutable;
using System.Diagnostics;
using System.Security.Claims;
using System.Text.Json;
using Microsoft.Extensions.Logging;
@ -158,6 +159,8 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Registration.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013));
// Ignore the response instance if a userinfo token was extracted.
if (!string.IsNullOrEmpty(context.UserinfoToken))
{
@ -171,11 +174,6 @@ public static partial class OpenIddictClientHandlers
context.Registration.TokenValidationParameters.NameClaimType,
context.Registration.TokenValidationParameters.RoleClaimType);
// Resolve the issuer that will be attached to the claims created by this handler.
// Note: at this stage, the optional issuer extracted from the response is assumed
// to be valid, as it is guarded against unknown values by the ValidateIssuer handler.
var issuer = (string?) context.Response[Claims.Issuer] ?? context.Configuration.Issuer!.AbsoluteUri;
foreach (var parameter in context.Response.GetParameters())
{
// Always exclude null keys as they can't be represented as valid claims.
@ -209,11 +207,11 @@ public static partial class OpenIddictClientHandlers
// Top-level claims represented as arrays are split and mapped to multiple CLR claims
// to match the logic implemented by IdentityModel for JWT token deserialization.
case { ValueKind: JsonValueKind.Array } value:
identity.AddClaims(parameter.Key, value, issuer);
identity.AddClaims(parameter.Key, value, context.Registration.Issuer.AbsoluteUri);
break;
case { ValueKind: _ } value:
identity.AddClaim(parameter.Key, value, issuer);
identity.AddClaim(parameter.Key, value, context.Registration.Issuer.AbsoluteUri);
break;
}
}

159
src/OpenIddict.Client/OpenIddictClientHandlers.cs

@ -93,6 +93,9 @@ public static partial class OpenIddictClientHandlers
ValidateUserinfoTokenWellknownClaims.Descriptor,
ValidateUserinfoTokenSubject.Descriptor,
PopulateMergedPrincipal.Descriptor,
MapStandardWebServicesFederationClaims.Descriptor,
/*
* Challenge processing:
*/
@ -3892,6 +3895,162 @@ public static partial class OpenIddictClientHandlers
}
}
/// <summary>
/// Contains the logic responsible for populating the merged principal from the other available principals.
/// </summary>
public sealed class PopulateMergedPrincipal : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<PopulateMergedPrincipal>()
.SetOrder(100_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Registration.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013));
context.MergedPrincipal = context.EndpointType switch
{
// Create a composite principal containing claims resolved from the frontchannel
// and backchannel identity tokens and the userinfo token principal, if available.
OpenIddictClientEndpointType.Redirection => CreateMergedPrincipal(
context.FrontchannelIdentityTokenPrincipal,
context.BackchannelIdentityTokenPrincipal,
context.UserinfoTokenPrincipal),
OpenIddictClientEndpointType.PostLogoutRedirection
=> context.StateTokenPrincipal?.Clone() ?? new ClaimsPrincipal(new ClaimsIdentity()),
_ => new ClaimsPrincipal(new ClaimsIdentity())
};
// Attach the registration identifier and identity of the authorization server to the returned principal to allow
// resolving it even if no other claim was added (e.g if no id_token was returned/no userinfo endpoint is available).
context.MergedPrincipal.SetClaim(Claims.AuthorizationServer, context.Registration.Issuer.AbsoluteUri)
.SetClaim(Claims.Private.RegistrationId, context.Registration.RegistrationId)
.SetClaim(Claims.Private.ProviderName, context.Registration.ProviderName);
return default;
ClaimsPrincipal CreateMergedPrincipal(params ClaimsPrincipal?[] principals)
{
// Note: the OpenIddict client can be used as a pure OAuth 2.0 authorization stack for
// delegation scenarios where the identity of the user is not needed. In this case,
// since no principal can be resolved from a token or a userinfo response to construct
// a user identity, a fake one containing an "unauthenticated" identity (i.e with its
// AuthenticationType property deliberately left to null) is used to allow the host
// to return a "successful" authentication result for these delegation-only scenarios.
if (!Array.Exists(principals, static principal => principal?.Identity is ClaimsIdentity { IsAuthenticated: true }))
{
return new ClaimsPrincipal(new ClaimsIdentity());
}
// Create a new composite identity containing the claims of all the principals.
//
// Note: if WS-Federation claim mapping was not disabled, the resulting identity
// will use the default WS-Federation claims as the name/role claim types.
var identity = context.Options.DisableWebServicesFederationClaimMapping ?
new ClaimsIdentity(
context.Registration.TokenValidationParameters.AuthenticationType,
context.Registration.TokenValidationParameters.NameClaimType,
context.Registration.TokenValidationParameters.RoleClaimType) :
new ClaimsIdentity(context.Registration.TokenValidationParameters.AuthenticationType);
foreach (var principal in principals)
{
// Note: the principal may be null if no value was extracted from the corresponding token.
if (principal is null)
{
continue;
}
foreach (var claim in principal.Claims)
{
// If a claim with the same type and the same value already exist, skip it.
if (identity.HasClaim(claim.Type, claim.Value))
{
continue;
}
identity.AddClaim(claim);
}
}
return new ClaimsPrincipal(identity);
}
}
}
/// <summary>
/// Contains the logic responsible for mapping select standard claims to their WS-Federation equivalent, if applicable.
/// </summary>
public sealed class MapStandardWebServicesFederationClaims : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireWebServicesFederationClaimMappingEnabled>()
.UseSingletonHandler<MapStandardWebServicesFederationClaims>()
.SetOrder(PopulateMergedPrincipal.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Registration.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013));
// As an OpenID Connect framework, the OpenIddict client mostly uses the claim set defined by the OpenID
// Connect core specification (https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims).
// While these claims can be easily accessed using their standard OIDC name, many components still use
// the Web Services Federation claims exposed by the BCL ClaimTypes class, sometimes without allowing
// to use different claim types (e.g ASP.NET Core Identity hardcodes ClaimTypes.NameIdentifier in a few
// places, like the GetUserId() extension). To reduce the difficulty of using the OpenIddict client with
// these components relying on WS-Federation-style claims, OpenIddict >= 4.7 integrates a built-in
// event handler that maps standard OpenID Connect claims to their Web Services Federation equivalent
// but deliberately doesn't remove the OpenID Connect claims from the resulting claims principal.
//
// Note: a similar event handler exists in OpenIddict.Client.WebIntegration to map these claims
// from non-standard/provider-specific claim types (see MapCustomWebServicesFederationClaims).
var issuer = context.Registration.Issuer.AbsoluteUri;
context.MergedPrincipal
.SetClaim(ClaimTypes.Email, context.MergedPrincipal.GetClaim(Claims.Email), issuer)
.SetClaim(ClaimTypes.Gender, context.MergedPrincipal.GetClaim(Claims.Gender), issuer)
.SetClaim(ClaimTypes.GivenName, context.MergedPrincipal.GetClaim(Claims.GivenName), issuer)
.SetClaim(ClaimTypes.Name, context.MergedPrincipal.GetClaim(Claims.PreferredUsername), issuer)
.SetClaim(ClaimTypes.NameIdentifier, context.MergedPrincipal.GetClaim(Claims.Subject), issuer)
.SetClaim(ClaimTypes.OtherPhone, context.MergedPrincipal.GetClaim(Claims.PhoneNumber), issuer)
.SetClaim(ClaimTypes.Surname, context.MergedPrincipal.GetClaim(Claims.FamilyName), issuer);
// 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.MergedPrincipal.SetClaim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider",
context.MergedPrincipal.GetClaim(Claims.Private.ProviderName));
return default;
}
}
/// <summary>
/// Contains the logic responsible for rejecting invalid challenge demands.
/// </summary>

12
src/OpenIddict.Client/OpenIddictClientOptions.cs

@ -5,6 +5,7 @@
*/
using System.ComponentModel;
using System.Security.Claims;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
@ -124,6 +125,17 @@ public sealed class OpenIddictClientOptions
/// </summary>
public bool DisableTokenStorage { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the claim mapping feature inferring
/// WS-Federation claims (exposed by the <see cref="ClaimTypes"/> class) from their
/// OpenID Connect/JSON Web Token or provider-specific equivalent should be disabled.
/// </summary>
/// <remarks>
/// Note: if automatic claim mapping is disabled, no WS-Federation claim will
/// be added to <see cref="ProcessAuthenticationContext.MergedPrincipal"/>.
/// </remarks>
public bool DisableWebServicesFederationClaimMapping { get; set; }
/// <summary>
/// Gets the OAuth 2.0 code challenge methods enabled for this application.
/// By default, only the S256 method is allowed (if the code flow is enabled).

32
src/OpenIddict.Client/OpenIddictClientService.cs

@ -348,12 +348,7 @@ public sealed class OpenIddictClientService
FrontchannelAccessToken = context.FrontchannelAccessToken,
FrontchannelIdentityToken = context.FrontchannelIdentityToken,
FrontchannelIdentityTokenPrincipal = context.FrontchannelIdentityTokenPrincipal,
Principal = OpenIddictHelpers.CreateMergedPrincipal(context.FrontchannelIdentityTokenPrincipal,
context.BackchannelIdentityTokenPrincipal,
context.UserinfoTokenPrincipal)
.SetClaim(Claims.AuthorizationServer, context.Registration.Issuer.AbsoluteUri)
.SetClaim(Claims.Private.RegistrationId, context.Registration.RegistrationId)
.SetClaim(Claims.Private.ProviderName, context.Registration.ProviderName),
Principal = context.MergedPrincipal,
Properties = context.Properties,
RefreshToken = context.RefreshToken,
StateTokenPrincipal = context.StateTokenPrincipal,
@ -597,11 +592,7 @@ public sealed class OpenIddictClientService
AccessToken = context.BackchannelAccessToken!,
IdentityToken = context.BackchannelIdentityToken,
IdentityTokenPrincipal = context.BackchannelIdentityTokenPrincipal,
Principal = OpenIddictHelpers.CreateMergedPrincipal(context.BackchannelIdentityTokenPrincipal,
context.UserinfoTokenPrincipal)
.SetClaim(Claims.AuthorizationServer, context.Registration.Issuer.AbsoluteUri)
.SetClaim(Claims.Private.RegistrationId, context.Registration.RegistrationId)
.SetClaim(Claims.Private.ProviderName, context.Registration.ProviderName),
Principal = context.MergedPrincipal,
Properties = context.Properties,
RefreshToken = context.RefreshToken,
TokenResponse = context.TokenResponse,
@ -769,11 +760,7 @@ public sealed class OpenIddictClientService
AccessToken = context.BackchannelAccessToken!,
IdentityToken = context.BackchannelIdentityToken,
IdentityTokenPrincipal = context.BackchannelIdentityTokenPrincipal,
Principal = OpenIddictHelpers.CreateMergedPrincipal(context.BackchannelIdentityTokenPrincipal,
context.UserinfoTokenPrincipal)
.SetClaim(Claims.AuthorizationServer, context.Registration.Issuer.AbsoluteUri)
.SetClaim(Claims.Private.RegistrationId, context.Registration.RegistrationId)
.SetClaim(Claims.Private.ProviderName, context.Registration.ProviderName),
Principal = context.MergedPrincipal,
Properties = context.Properties,
RefreshToken = context.RefreshToken,
TokenResponse = context.TokenResponse ?? new(),
@ -1117,11 +1104,7 @@ public sealed class OpenIddictClientService
AccessToken = context.BackchannelAccessToken!,
IdentityToken = context.BackchannelIdentityToken,
IdentityTokenPrincipal = context.BackchannelIdentityTokenPrincipal,
Principal = OpenIddictHelpers.CreateMergedPrincipal(context.BackchannelIdentityTokenPrincipal,
context.UserinfoTokenPrincipal)
.SetClaim(Claims.AuthorizationServer, context.Registration.Issuer.AbsoluteUri)
.SetClaim(Claims.Private.RegistrationId, context.Registration.RegistrationId)
.SetClaim(Claims.Private.ProviderName, context.Registration.ProviderName),
Principal = context.MergedPrincipal,
Properties = context.Properties,
RefreshToken = context.RefreshToken,
TokenResponse = context.TokenResponse,
@ -1282,12 +1265,7 @@ public sealed class OpenIddictClientService
AccessToken = context.BackchannelAccessToken!,
IdentityToken = context.BackchannelIdentityToken,
IdentityTokenPrincipal = context.BackchannelIdentityTokenPrincipal,
Principal = OpenIddictHelpers.CreateMergedPrincipal(
context.BackchannelIdentityTokenPrincipal,
context.UserinfoTokenPrincipal)
.SetClaim(Claims.AuthorizationServer, context.Registration.Issuer.AbsoluteUri)
.SetClaim(Claims.Private.RegistrationId, context.Registration.RegistrationId)
.SetClaim(Claims.Private.ProviderName, context.Registration.ProviderName),
Principal = context.MergedPrincipal,
Properties = context.Properties,
RefreshToken = context.RefreshToken,
TokenResponse = context.TokenResponse,

7
src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs

@ -424,11 +424,8 @@ public static partial class OpenIddictValidationHandlers
context.Options.TokenValidationParameters.RoleClaimType);
// Resolve the issuer that will be attached to the claims created by this handler.
// Note: at this stage, the optional issuer extracted from the response is assumed
// to be valid, as it is guarded against unknown values by the ValidateIssuer handler.
var issuer = (string?) context.Response[Claims.Issuer] ??
context.Configuration.Issuer?.AbsoluteUri ??
context.BaseUri?.AbsoluteUri ?? ClaimsIdentity.DefaultIssuer;
var issuer = context.Configuration.Issuer?.AbsoluteUri ??
context.BaseUri?.AbsoluteUri ?? ClaimsIdentity.DefaultIssuer;
foreach (var parameter in context.Response.GetParameters())
{

Loading…
Cancel
Save