diff --git a/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs b/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs index 3da86db5..28bc8224 100644 --- a/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs +++ b/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs @@ -52,6 +52,7 @@ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.InteropServices; +using System.Security.Claims; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -224,6 +225,22 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder return Set(registration => registration.Scopes.UnionWith(scopes)); } + /// + /// Sets the issuer that will be attached to the + /// instances created by the OpenIddict client stack for this provider. + /// + /// The claims issuer. + /// The instance. + public {{ provider.name }} SetClaimsIssuer(string issuer) + { + if (string.IsNullOrEmpty(issuer)) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0124), nameof(issuer)); + } + + return Set(registration => registration.ClaimsIssuer = issuer); + } + /// /// Sets the provider name. /// diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs index d040b712..a2a4f32b 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs @@ -1255,6 +1255,11 @@ public static partial class OpenIddictClientWebIntegrationHandlers context.Registration.TokenValidationParameters.NameClaimType, context.Registration.TokenValidationParameters.RoleClaimType); + // Resolve the issuer that will be attached to the claims created by this handler. + var issuer = context.Registration.ClaimsIssuer ?? + context.Registration.ProviderName ?? + context.Registration.Issuer.AbsoluteUri; + foreach (var parameter in parameters) { // Note: in the typical case, the response parameters should be deserialized from a @@ -1269,11 +1274,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, context.Registration.Issuer.AbsoluteUri); + identity.AddClaims(parameter.Key, value, issuer); break; case { ValueKind: _ } value: - identity.AddClaim(parameter.Key, value, context.Registration.Issuer.AbsoluteUri); + identity.AddClaim(parameter.Key, value, issuer); break; } } @@ -1323,7 +1328,9 @@ public static partial class OpenIddictClientWebIntegrationHandlers // 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; + var issuer = context.Registration.ClaimsIssuer ?? + context.Registration.ProviderName ?? + context.Registration.Issuer.AbsoluteUri; context.MergedPrincipal.SetClaim(ClaimTypes.Email, issuer: issuer, value: context.Registration.ProviderType switch { diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Introspection.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Introspection.cs index 12229752..77524e1b 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.Introspection.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.Introspection.cs @@ -406,6 +406,11 @@ public static partial class OpenIddictClientHandlers nameType: ClaimTypes.Name, roleType: ClaimTypes.Role); + // Resolve the issuer that will be attached to the claims created by this handler. + var issuer = context.Registration.ClaimsIssuer ?? + context.Registration.ProviderName ?? + context.Registration.Issuer.AbsoluteUri; + foreach (var parameter in context.Response.GetParameters()) { // Always exclude null keys as they can't be represented as valid claims. @@ -439,11 +444,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, context.Registration.Issuer.AbsoluteUri); + identity.AddClaims(parameter.Key, value, issuer); break; case { ValueKind: _ } value: - identity.AddClaim(parameter.Key, value, context.Registration.Issuer.AbsoluteUri); + identity.AddClaim(parameter.Key, value, issuer); break; } } diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs index 98618bac..11ac46d3 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs @@ -399,15 +399,42 @@ public static partial class OpenIddictClientHandlers token = token.InnerToken; } - // Clone the identity and remove OpenIddict-specific claims from tokens that are not fully trusted. - var identity = result.ClaimsIdentity.Clone(claim => claim switch + ClaimsIdentity identity; + + // If the token is not a state token and a different claims issuer value was set, + // override the issuer attached to all the claims returned by IdentityModel. + if (result.TokenType is not JsonWebTokenTypes.Private.StateToken && + (context.Registration.ClaimsIssuer ?? context.Registration.ProviderName) is { Length: > 0 } issuer && + !string.Equals(issuer, context.Registration.Issuer?.AbsoluteUri, StringComparison.Ordinal)) { - // Exclude claims starting with "oi_", unless the token is a state token. - { Type: string type } when type.StartsWith(Claims.Prefixes.Private) && - result.TokenType is not JsonWebTokenTypes.Private.StateToken => false, + identity = new ClaimsIdentity( + result.ClaimsIdentity.AuthenticationType, + result.ClaimsIdentity.NameClaimType, + result.ClaimsIdentity.RoleClaimType); - _ => true // Allow any other claim. - }); + foreach (var claim in result.ClaimsIdentity.Claims) + { + // Exclude claims starting with "oi_" from tokens that are not fully trusted. + if (claim.Type.StartsWith(Claims.Prefixes.Private)) + { + continue; + } + + identity.AddClaim(new Claim(claim.Type, claim.Value, claim.ValueType, issuer, issuer, identity)); + } + } + + else + { + identity = result.ClaimsIdentity.Clone(claim => claim switch + { + // Exclude claims starting with "oi_", unless the token is a state token. + { Type: string type } when type.StartsWith(Claims.Prefixes.Private) && + result.TokenType is not JsonWebTokenTypes.Private.StateToken => false, + + _ => true // Allow any other claim. + }); + } if (context.ValidTokenTypes.Contains(TokenTypeHints.StateToken)) { diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Userinfo.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Userinfo.cs index 0932e2ca..e974aaf5 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.Userinfo.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.Userinfo.cs @@ -175,6 +175,11 @@ 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. + var issuer = context.Registration.ClaimsIssuer ?? + context.Registration.ProviderName ?? + context.Registration.Issuer.AbsoluteUri; + foreach (var parameter in context.Response.GetParameters()) { // Always exclude null keys as they can't be represented as valid claims. @@ -208,11 +213,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, context.Registration.Issuer.AbsoluteUri); + identity.AddClaims(parameter.Key, value, issuer); break; case { ValueKind: _ } value: - identity.AddClaim(parameter.Key, value, context.Registration.Issuer.AbsoluteUri); + identity.AddClaim(parameter.Key, value, issuer); break; } } diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.cs index e10b0dc4..f327891d 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.cs @@ -4333,7 +4333,9 @@ 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). - var issuer = context.Registration.Issuer.AbsoluteUri; + var issuer = context.Registration.ClaimsIssuer ?? + context.Registration.ProviderName ?? + context.Registration.Issuer.AbsoluteUri; context.MergedPrincipal .SetClaim(ClaimTypes.Email, context.MergedPrincipal.GetClaim(Claims.Email), issuer) @@ -6799,7 +6801,9 @@ 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. - var issuer = context.Registration.Issuer.AbsoluteUri; + var issuer = context.Registration.ClaimsIssuer ?? + context.Registration.ProviderName ?? + context.Registration.Issuer.AbsoluteUri; context.Principal .SetClaim(ClaimTypes.Name, context.Principal.GetClaim(Claims.Username), issuer) diff --git a/src/OpenIddict.Client/OpenIddictClientRegistration.cs b/src/OpenIddict.Client/OpenIddictClientRegistration.cs index 80ed1ff9..5e973812 100644 --- a/src/OpenIddict.Client/OpenIddictClientRegistration.cs +++ b/src/OpenIddict.Client/OpenIddictClientRegistration.cs @@ -5,6 +5,7 @@ */ using System.Diagnostics; +using System.Security.Claims; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Tokens; @@ -113,6 +114,16 @@ public sealed class OpenIddictClientRegistration /// public HashSet ResponseTypes { get; } = new(StringComparer.Ordinal); + /// + /// Gets or sets the issuer that will be attached to the + /// instances created by the OpenIddict client stack for this registration. + /// + /// + /// Note: if this property is not explicitly set, the provider name (if set) + /// or the issuer URI are automatically used as a fallback value. + /// + public string? ClaimsIssuer { get; set; } + /// /// Gets or sets the URI of the authorization server. /// diff --git a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs index dcf6597c..638600e1 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs @@ -8,6 +8,7 @@ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.InteropServices; +using System.Security.Claims; using System.Security.Cryptography.X509Certificates; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.IdentityModel.Tokens; @@ -665,6 +666,22 @@ public sealed class OpenIddictValidationBuilder return Configure(options => options.Configuration = configuration); } + /// + /// Sets the issuer that will be attached to the + /// instances created by the OpenIddict validation stack. + /// + /// The claims issuer. + /// The instance. + public OpenIddictValidationBuilder SetClaimsIssuer(string issuer) + { + if (string.IsNullOrEmpty(issuer)) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0124), nameof(issuer)); + } + + return Configure(options => options.ClaimsIssuer = issuer); + } + /// /// Sets the client identifier client_id used when communicating /// with the remote authorization server (e.g for introspection). diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs index e95c12bc..afd4a285 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs @@ -397,7 +397,8 @@ public static partial class OpenIddictValidationHandlers context.Options.TokenValidationParameters.RoleClaimType); // Resolve the issuer that will be attached to the claims created by this handler. - var issuer = context.Configuration.Issuer?.AbsoluteUri ?? + var issuer = context.Options.ClaimsIssuer ?? + context.Configuration.Issuer?.AbsoluteUri ?? context.BaseUri?.AbsoluteUri ?? ClaimsIdentity.DefaultIssuer; foreach (var parameter in context.Response.GetParameters()) diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs index 5b665116..f641db6b 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs @@ -334,9 +334,32 @@ public static partial class OpenIddictValidationHandlers return; } + ClaimsIdentity identity; + + // If a different claims issuer value was set, override the + // issuer attached to all the claims returned by IdentityModel. + if (context.Options.ClaimsIssuer is { Length: > 0 } issuer && + !string.Equals(issuer, (context.Configuration.Issuer ?? context.BaseUri)?.AbsoluteUri, StringComparison.Ordinal)) + { + identity = new ClaimsIdentity( + result.ClaimsIdentity.AuthenticationType, + result.ClaimsIdentity.NameClaimType, + result.ClaimsIdentity.RoleClaimType); + + foreach (var claim in result.ClaimsIdentity.Claims) + { + identity.AddClaim(new Claim(claim.Type, claim.Value, claim.ValueType, issuer, issuer, identity)); + } + } + + else + { + identity = result.ClaimsIdentity; + } + // Attach the principal extracted from the token to the parent event context and store // the token type (resolved from "typ" or "token_usage") as a special private claim. - context.Principal = new ClaimsPrincipal(result.ClaimsIdentity).SetTokenType(result.TokenType switch + context.Principal = new ClaimsPrincipal(identity).SetTokenType(result.TokenType switch { null or { Length: 0 } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0025)), diff --git a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs index eb3f8432..59b82fe2 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs @@ -4,6 +4,7 @@ * the license and the contributors participating to this project. */ +using System.Security.Claims; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Tokens; @@ -102,6 +103,16 @@ public sealed class OpenIddictValidationOptions /// public bool EnableTokenEntryValidation { get; set; } + /// + /// Gets or sets the issuer that will be attached to the + /// instances created by the OpenIddict validation stack. + /// + /// + /// Note: if this property is not explicitly set, the + /// issuer URI is automatically used as a fallback value. + /// + public string? ClaimsIssuer { get; set; } + /// /// Gets or sets the absolute URI of the OAuth 2.0/OpenID Connect server. ///