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.
///