diff --git a/src/OpenIddict.Abstractions/OpenIddictConstants.cs b/src/OpenIddict.Abstractions/OpenIddictConstants.cs
index 6ce389f54..d1abe08d3 100644
--- a/src/OpenIddict.Abstractions/OpenIddictConstants.cs
+++ b/src/OpenIddict.Abstractions/OpenIddictConstants.cs
@@ -261,7 +261,9 @@ public static class GrantTypes
public static class JsonWebTokenTypes
{
public const string AccessToken = "at+jwt";
- public const string Jwt = "JWT";
+ public const string AuthorizationGrant = "authorization-grant+jwt";
+ public const string ClientAuthentication = "client-authentication+jwt";
+ public const string GenericJsonWebToken = "JWT";
public static class Prefixes
{
diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx
index 5d37c9203..803a1fb07 100644
--- a/src/OpenIddict.Abstractions/OpenIddictResources.resx
+++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx
@@ -1796,6 +1796,9 @@ Alternatively, any value respecting the '[region]-[subregion]-[identifier]' patt
The '{0}' parameter cannot contain values that are not valid absolute URIs containing no fragment component.
+
+ The issuer cannot be retrieved from the server options or inferred from the current request or is not a valid value.
+
The security token is missing.
diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs
index f0dcbc882..af1705b67 100644
--- a/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs
+++ b/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs
@@ -80,7 +80,8 @@ public ValueTask HandleAsync(ValidateTokenContext context)
//
// See https://datatracker.ietf.org/doc/html/draft-bradley-oauth-jwt-encoded-state-09#section-4.3
// for more information.
- if (context.ValidTokenTypes.Count > 1 && context.ValidTokenTypes.Contains(TokenTypeIdentifiers.Private.StateToken))
+ if (context.ValidTokenTypes.Count is > 1 &&
+ context.ValidTokenTypes.Contains(TokenTypeIdentifiers.Private.StateToken))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0308));
}
@@ -1109,10 +1110,12 @@ public ValueTask HandleAsync(GenerateTokenContext context)
{
null or { Length: 0 } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0025)),
- // For client assertions, use the generic "JWT" type.
- TokenTypeIdentifiers.Private.ClientAssertion => JsonWebTokenTypes.Jwt,
+ // Note: OpenIddict 7.0 and higher no uses the generic "JWT" value for client assertions
+ // but uses the new standard "client-authentication+jwt" type instead, as defined in the
+ // https://www.ietf.org/archive/id/draft-ietf-oauth-rfc7523bis-01.html#name-updates-to-rfc-7523
+ // specification.
+ TokenTypeIdentifiers.Private.ClientAssertion => JsonWebTokenTypes.ClientAuthentication,
- // For state tokens, use its private representation.
TokenTypeIdentifiers.Private.StateToken => JsonWebTokenTypes.Private.StateToken,
string value => value
diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.cs
index 8eed1c9e2..fa6bbb4f7 100644
--- a/src/OpenIddict.Client/OpenIddictClientHandlers.cs
+++ b/src/OpenIddict.Client/OpenIddictClientHandlers.cs
@@ -2646,21 +2646,20 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context)
principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value);
}
- // Use the URI of the token endpoint as the audience, as recommended by the specifications.
- // Applications that need to use a different value can register a custom event handler.
+ // Important: the initial OpenID Connect and Assertion Framework for OAuth 2.0 Client Authentication
+ // specifications initially encouraged using the token endpoint URI as the client assertion audience.
+ // Unfortunately, it was determined in 2025 that using the token endpoint URI could allow a malicious
+ // identity provider to trick a legitimate client into using attacker-controlled values as audiences,
+ // including token endpoint URIs or issuer identifiers used by other authorization servers, which could
+ // result in impersonation attacks if the same set of credentials were used to generate the assertions
+ // for all the client registrations (which is not a recommended pattern in OpenIddict). To mitigate that,
+ // OpenIddict no longer allows uses the token endpoint URI and always uses the issuer identity instead.
+ // Unlike the token endpoint URI, the issuer returned by the authorization server in its configuration
+ // document is always validated and must exactly match the value expected by the client application.
//
- // See https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
- // and https://datatracker.ietf.org/doc/html/rfc7523#section-3 for more information.
- if (!string.IsNullOrEmpty(context.TokenEndpoint?.OriginalString))
- {
- principal.SetAudiences(context.TokenEndpoint.OriginalString);
- }
-
- // If the token endpoint URI is not available, use the issuer URI as the audience.
- else
- {
- principal.SetAudiences(context.Registration.Issuer.OriginalString);
- }
+ // For more information, see https://www.ietf.org/archive/id/draft-ietf-oauth-rfc7523bis-01.html#name-updates-to-rfc-7521
+ // and https://openid.net/wp-content/uploads/2025/01/OIDF-Responsible-Disclosure-Notice-on-Security-Vulnerability-for-private_key_jwt.pdf.
+ principal.SetAudiences(context.Registration.Issuer.OriginalString);
// Use the client_id as both the subject and the issuer, as required by the specifications.
//
diff --git a/src/OpenIddict.Client/OpenIddictClientOptions.cs b/src/OpenIddict.Client/OpenIddictClientOptions.cs
index 5f41e6370..8f396b4e0 100644
--- a/src/OpenIddict.Client/OpenIddictClientOptions.cs
+++ b/src/OpenIddict.Client/OpenIddictClientOptions.cs
@@ -113,6 +113,20 @@ public sealed class OpenIddictClientOptions
ClockSkew = TimeSpan.Zero,
NameClaimType = Claims.Name,
RoleClaimType = Claims.Role,
+ // Note: unlike IdentityModel, this custom validator deliberately uses case-insensitive comparisons.
+ TypeValidator = static (type, token, parameters) =>
+ {
+ if (parameters.ValidTypes is not null && parameters.ValidTypes.Any() &&
+ !parameters.ValidTypes.Contains(type, StringComparer.OrdinalIgnoreCase))
+ {
+ throw new SecurityTokenInvalidTypeException(SR.GetResourceString(SR.ID0271))
+ {
+ InvalidType = type
+ };
+ }
+
+ return type;
+ },
// Note: audience and lifetime are manually validated by OpenIddict itself.
ValidateAudience = false,
ValidateLifetime = false
diff --git a/src/OpenIddict.Client/OpenIddictClientRegistration.cs b/src/OpenIddict.Client/OpenIddictClientRegistration.cs
index 48e6c8d4f..b77572e47 100644
--- a/src/OpenIddict.Client/OpenIddictClientRegistration.cs
+++ b/src/OpenIddict.Client/OpenIddictClientRegistration.cs
@@ -190,6 +190,20 @@ public sealed class OpenIddictClientRegistration
ClockSkew = TimeSpan.Zero,
NameClaimType = Claims.Name,
RoleClaimType = Claims.Role,
+ // Note: unlike IdentityModel, this custom validator deliberately uses case-insensitive comparisons.
+ TypeValidator = static (type, token, parameters) =>
+ {
+ if (parameters.ValidTypes is not null && parameters.ValidTypes.Any() &&
+ !parameters.ValidTypes.Contains(type, StringComparer.OrdinalIgnoreCase))
+ {
+ throw new SecurityTokenInvalidTypeException(SR.GetResourceString(SR.ID0271))
+ {
+ InvalidType = type
+ };
+ }
+
+ return type;
+ },
// Note: audience and lifetime are manually validated by OpenIddict itself.
ValidateAudience = false,
ValidateLifetime = false
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs
index 3e4aed39f..4d9509374 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs
+++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs
@@ -900,7 +900,7 @@ public ValueTask HandleAsync(ValidateAuthorizationRequestContext context)
// Prevent response_type=none from being used with any other value.
// See https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#none for more information.
var types = context.Request.GetResponseTypes().ToHashSet(StringComparer.Ordinal);
- if (types.Count > 1 && types.Contains(ResponseTypes.None))
+ if (types.Count is > 1 && types.Contains(ResponseTypes.None))
{
context.Logger.LogInformation(6212, SR.GetResourceString(SR.ID6212), context.Request.ResponseType);
@@ -2396,8 +2396,8 @@ public ValueTask HandleAsync(ApplyAuthorizationResponseContext context)
{
{ IsAbsoluteUri: true } uri => uri.AbsoluteUri,
- // At this stage, throw an exception if the issuer cannot be retrieved or is not valid.
- _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0023))
+ // Throw an exception if the issuer cannot be retrieved or is not valid.
+ _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0496))
};
}
@@ -2992,7 +2992,7 @@ public ValueTask HandleAsync(ValidatePushedAuthorizationRequestContext context)
// Prevent response_type=none from being used with any other value.
// See https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#none for more information.
var types = context.Request.GetResponseTypes().ToHashSet(StringComparer.Ordinal);
- if (types.Count > 1 && types.Contains(ResponseTypes.None))
+ if (types.Count is > 1 && types.Contains(ResponseTypes.None))
{
context.Logger.LogInformation(6260, SR.GetResourceString(SR.ID6260), context.Request.ResponseType);
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs
index 45e04bff6..bccd60b5c 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs
+++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs
@@ -343,7 +343,13 @@ public ValueTask HandleAsync(HandleConfigurationRequestContext context)
throw new ArgumentNullException(nameof(context));
}
- context.Issuer = context.Options.Issuer ?? context.BaseUri;
+ context.Issuer = (context.Options.Issuer ?? context.BaseUri) switch
+ {
+ { IsAbsoluteUri: true } uri => uri,
+
+ // Throw an exception if the issuer cannot be retrieved or is not valid.
+ _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0496))
+ };
return default;
}
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs
index 3f7f600a8..fc717c8cb 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs
+++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs
@@ -737,7 +737,13 @@ public ValueTask HandleAsync(HandleIntrospectionRequestContext context)
Debug.Assert(context.GenericTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
- context.Issuer = context.Options.Issuer ?? context.BaseUri;
+ context.Issuer = (context.Options.Issuer ?? context.BaseUri) switch
+ {
+ { IsAbsoluteUri: true } uri => uri,
+
+ // Throw an exception if the issuer cannot be retrieved or is not valid.
+ _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0496))
+ };
context.TokenId = context.GenericTokenPrincipal.GetClaim(Claims.JwtId);
context.Subject = context.GenericTokenPrincipal.GetClaim(Claims.Subject);
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs
index f3d4c338d..286dcb184 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs
+++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs
@@ -95,7 +95,7 @@ public ValueTask HandleAsync(ValidateTokenContext context)
// To simplify the token validation parameters selection logic, an exception is thrown
// if multiple token types are considered valid and contain tokens issued by the
// authorization server and tokens issued by the client (e.g client assertions).
- if (context.ValidTokenTypes.Count > 1 &&
+ if (context.ValidTokenTypes.Count is > 1 &&
context.ValidTokenTypes.Contains(TokenTypeIdentifiers.Private.ClientAssertion))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0308));
@@ -117,9 +117,34 @@ TokenValidationParameters GetClientTokenValidationParameters()
// Note: the audience/issuer/lifetime are manually validated by OpenIddict itself.
var parameters = new TokenValidationParameters
{
+ TypeValidator = static (type, token, parameters) =>
+ {
+ // Note: unlike IdentityModel, this custom validator deliberately uses case-insensitive comparisons.
+ if (parameters.ValidTypes is not null && parameters.ValidTypes.Any() &&
+ !parameters.ValidTypes.Contains(type, StringComparer.OrdinalIgnoreCase))
+ {
+ throw new SecurityTokenInvalidTypeException(SR.GetResourceString(SR.ID0271))
+ {
+ InvalidType = type
+ };
+ }
+
+ return type;
+ },
+
ValidateAudience = false,
ValidateIssuer = false,
- ValidateLifetime = false
+ ValidateLifetime = false,
+
+ // Note: OpenIddict 7.0 and higher no uses the generic "JWT" value for client assertions and
+ // requires using the new standard "client-authentication+jwt" type instead, as defined in the
+ // https://www.ietf.org/archive/id/draft-ietf-oauth-rfc7523bis-01.html#name-updates-to-rfc-7523
+ // draft. The longer "application/client-authentication+jwt" form is also considered valid.
+ ValidTypes =
+ [
+ JsonWebTokenTypes.ClientAuthentication,
+ JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.ClientAuthentication
+ ]
};
// Only provide a signing key resolver if the degraded mode was not enabled.
@@ -204,8 +229,8 @@ TokenValidationParameters GetServerTokenValidationParameters()
// For identity tokens, both "JWT" and "application/jwt" are valid.
TokenTypeIdentifiers.IdentityToken =>
[
- JsonWebTokenTypes.Jwt,
- JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.Jwt
+ JsonWebTokenTypes.GenericJsonWebToken,
+ JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.GenericJsonWebToken
],
// For authorization codes, only the short "oi_auc+jwt" form is valid.
@@ -529,23 +554,23 @@ 1 when context.ValidTokenTypes.Contains(TokenTypeIdentifiers.AccessToken)
// the token type (resolved from "typ" or "token_usage") as a special private claim.
context.Principal = new ClaimsPrincipal(result.ClaimsIdentity).SetTokenType(result.TokenType switch
{
- // Client assertions are typically created by client libraries with either a missing "typ" header
- // or a generic value like "JWT". Since the type defined by the client cannot be used as-is,
- // validation is bypassed and tokens used as client assertions are assumed to be client assertions.
- _ when context.ValidTokenTypes.Count is 1 &&
- context.ValidTokenTypes.Contains(TokenTypeIdentifiers.Private.ClientAssertion)
- => TokenTypeIdentifiers.Private.ClientAssertion,
-
null or { Length: 0 } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0025)),
- // Both at+jwt and application/at+jwt are supported for access tokens.
- JsonWebTokenTypes.AccessToken or JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.AccessToken
+ // Both "at+jwt" and "application/at+jwt" are supported for access tokens.
+ JsonWebTokenTypes.AccessToken or
+ JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.AccessToken
=> TokenTypeIdentifiers.AccessToken,
- // Both JWT and application/JWT are supported for identity tokens.
- JsonWebTokenTypes.Jwt or JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.Jwt
+ // Both "JWT" and "application/jwt" are supported for identity tokens.
+ JsonWebTokenTypes.GenericJsonWebToken or
+ JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.GenericJsonWebToken
=> TokenTypeIdentifiers.IdentityToken,
+ // Both "client-authentication+jwt" and "application/client-authentication+jwt" for client assertions.
+ JsonWebTokenTypes.ClientAuthentication or
+ JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.ClientAuthentication
+ => TokenTypeIdentifiers.Private.ClientAssertion,
+
JsonWebTokenTypes.Private.AuthorizationCode => TokenTypeIdentifiers.Private.AuthorizationCode,
JsonWebTokenTypes.Private.DeviceCode => TokenTypeIdentifiers.Private.DeviceCode,
JsonWebTokenTypes.Private.RefreshToken => TokenTypeIdentifiers.RefreshToken,
@@ -1629,7 +1654,7 @@ TokenTypeIdentifiers.RefreshToken or TokenTypeIdentifiers.Private.U
TokenTypeIdentifiers.AccessToken => JsonWebTokenTypes.AccessToken,
TokenTypeIdentifiers.Private.AuthorizationCode => JsonWebTokenTypes.Private.AuthorizationCode,
TokenTypeIdentifiers.Private.DeviceCode => JsonWebTokenTypes.Private.DeviceCode,
- TokenTypeIdentifiers.IdentityToken => JsonWebTokenTypes.Jwt,
+ TokenTypeIdentifiers.IdentityToken => JsonWebTokenTypes.GenericJsonWebToken,
TokenTypeIdentifiers.RefreshToken => JsonWebTokenTypes.Private.RefreshToken,
TokenTypeIdentifiers.Private.RequestToken => JsonWebTokenTypes.Private.RequestToken,
TokenTypeIdentifiers.Private.UserCode => JsonWebTokenTypes.Private.UserCode,
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs
index db6660550..84a4949a5 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs
+++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs
@@ -499,7 +499,14 @@ public ValueTask HandleAsync(HandleUserInfoRequestContext context)
Debug.Assert(context.AccessTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
- context.Issuer = context.Options.Issuer ?? context.BaseUri;
+ context.Issuer = (context.Options.Issuer ?? context.BaseUri) switch
+ {
+ { IsAbsoluteUri: true } uri => uri,
+
+ // Throw an exception if the issuer cannot be retrieved or is not valid.
+ _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0496))
+ };
+
context.Subject = context.AccessTokenPrincipal.GetClaim(Claims.Subject);
// The following claims are all optional and should be excluded when
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs
index 62957da99..b132de946 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs
+++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs
@@ -758,7 +758,7 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context)
return default;
}
- // Client assertions MUST contain at least one "aud" claim. For more information,
+ // Client assertions MUST contain an "aud" claim. For more information,
// see https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
// and https://datatracker.ietf.org/doc/html/rfc7523#section-3.
if (!context.ClientAssertionPrincipal.HasClaim(Claims.Audience))
@@ -789,19 +789,16 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context)
static bool ValidateClaimGroup(string name, List values) => name switch
{
// The following claims MUST be represented as unique strings.
- Claims.AuthorizedParty or Claims.Issuer or Claims.JwtId or Claims.Subject
+ //
+ // Important: client assertions with multiple audiences was initially deliberately supported by
+ // the OpenID Connect and Assertion Framework for OAuth 2.0 Client Authentication specifications.
+ // Since 2025, using multiple audiences is no longer allowed for security reasons. As such, the
+ // "aud" claim present in client assertions MUST always be represented as a single string.
+ //
+ // See https://www.ietf.org/archive/id/draft-ietf-oauth-rfc7523bis-01.html#section-4 for more information.
+ Claims.Audience or Claims.AuthorizedParty or Claims.Issuer or Claims.JwtId or Claims.Subject
=> values is [{ ValueType: ClaimValueTypes.String }],
- // The following claims MUST be represented as unique strings or array of strings.
- Claims.Audience
- => values.TrueForAll(static value => value.ValueType is ClaimValueTypes.String) ||
- // Note: a unique claim using the special JSON_ARRAY claim value type is allowed
- // if the individual elements of the parsed JSON array are all string values.
- (values is [{ ValueType: JsonClaimValueTypes.JsonArray, Value: string value }] &&
- JsonSerializer.Deserialize(value, OpenIddictSerializer.Default.JsonElement)
- is { ValueKind: JsonValueKind.Array } element &&
- OpenIddictHelpers.ValidateArrayElements(element, JsonValueKind.String)),
-
// The following claims MUST be represented as unique numeric dates.
Claims.ExpiresAt or Claims.IssuedAt or Claims.NotBefore
=> values is [{ ValueType: ClaimValueTypes.Integer or ClaimValueTypes.Integer32 or
@@ -916,10 +913,15 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context)
Debug.Assert(context.ClientAssertionPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
- // Ensure at least one non-empty audience was specified (note: in
- // the most common case, a single audience is generally specified).
- var audiences = context.ClientAssertionPrincipal.GetClaims(Claims.Audience);
- if (!audiences.Any(static audience => !string.IsNullOrEmpty(audience)))
+ // Important: client assertions with multiple audiences was initially deliberately supported by
+ // the OpenID Connect and Assertion Framework for OAuth 2.0 Client Authentication specifications.
+ // Since 2025, using multiple audiences is no longer allowed for security reasons: as such, a single
+ // audience is allowed here and an exception is thrown if multiple claims are present in the principal.
+ //
+ // See https://www.ietf.org/archive/id/draft-ietf-oauth-rfc7523bis-01.html#section-4 for more information.
+ var audience = context.ClientAssertionPrincipal.GetClaim(Claims.Audience);
+ if (string.IsNullOrEmpty(audience) ||
+ !Uri.TryCreate(audience, UriKind.Absolute, out Uri? uri) || OpenIddictHelpers.IsImplicitFileUri(uri))
{
context.Reject(
error: Errors.InvalidGrant,
@@ -929,8 +931,14 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context)
return default;
}
- // Ensure at least one of the audiences points to the current authorization server.
- if (!ValidateAudiences(audiences))
+ // Throw an exception if the issuer cannot be retrieved or is not valid.
+ var issuer = context.Options.Issuer ?? context.BaseUri;
+ if (issuer is not { IsAbsoluteUri: true })
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0496));
+ }
+
+ if (!UriEquals(uri, issuer))
{
context.Reject(
error: Errors.InvalidGrant,
@@ -942,75 +950,6 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context)
return default;
- bool ValidateAudiences(ImmutableArray audiences)
- {
- foreach (var audience in audiences)
- {
- // Ignore the iterated audience if it's not a valid absolute URI.
- if (!Uri.TryCreate(audience, UriKind.Absolute, out Uri? uri) || OpenIddictHelpers.IsImplicitFileUri(uri))
- {
- continue;
- }
-
- // Consider the audience valid if it matches the issuer value assigned to the current instance.
- //
- // See https://datatracker.ietf.org/doc/html/rfc7523#section-3 for more information.
- if (context.Options.Issuer is not null && UriEquals(uri, context.Options.Issuer))
- {
- return true;
- }
-
- // At this point, ignore the rest of the validation logic if the current base URI is not known.
- if (context.BaseUri is null)
- {
- continue;
- }
-
- // Consider the audience valid if it matches the current base URI, unless an explicit issuer was set.
- if (context.Options.Issuer is null && UriEquals(uri, context.BaseUri))
- {
- return true;
- }
-
- // Consider the audience valid if it matches one of the URIs assigned to the token
- // endpoint, independently of whether the request is a token request or not.
- if (MatchesAnyUri(uri, context.Options.TokenEndpointUris))
- {
- return true;
- }
-
- // Consider the audience valid if it matches one of the URIs
- // assigned to the endpoint that received the request.
- switch (context.EndpointType)
- {
- case OpenIddictServerEndpointType.DeviceAuthorization
- when MatchesAnyUri(uri, context.Options.DeviceAuthorizationEndpointUris):
- case OpenIddictServerEndpointType.Introspection
- when MatchesAnyUri(uri, context.Options.IntrospectionEndpointUris):
- case OpenIddictServerEndpointType.PushedAuthorization
- when MatchesAnyUri(uri, context.Options.PushedAuthorizationEndpointUris):
- case OpenIddictServerEndpointType.Revocation
- when MatchesAnyUri(uri, context.Options.RevocationEndpointUris):
- return true;
- }
- }
-
- return false;
- }
-
- bool MatchesAnyUri(Uri uri, List uris)
- {
- for (var index = 0; index < uris.Count; index++)
- {
- if (UriEquals(uri, OpenIddictHelpers.CreateAbsoluteUri(context.BaseUri, uris[index])))
- {
- return true;
- }
- }
-
- return false;
- }
-
static bool UriEquals(Uri left, Uri right)
{
if (string.Equals(left.AbsolutePath, right.AbsolutePath, StringComparison.Ordinal))
@@ -3541,7 +3480,13 @@ public async ValueTask HandleAsync(ProcessSignInContext context)
}
// Use the server identity as the token issuer.
- principal.SetClaim(Claims.Private.Issuer, (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri);
+ principal.SetClaim(Claims.Private.Issuer, (context.Options.Issuer ?? context.BaseUri) switch
+ {
+ { IsAbsoluteUri: true } uri => uri.AbsoluteUri,
+
+ // Throw an exception if the issuer cannot be retrieved or is not valid.
+ _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0496))
+ });
// Set the audiences based on the resource claims stored in the principal.
principal.SetAudiences(context.Principal.GetResources());
@@ -3665,7 +3610,13 @@ public async ValueTask HandleAsync(ProcessSignInContext context)
}
// Use the server identity as the token issuer.
- principal.SetClaim(Claims.Private.Issuer, (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri);
+ principal.SetClaim(Claims.Private.Issuer, (context.Options.Issuer ?? context.BaseUri) switch
+ {
+ { IsAbsoluteUri: true } uri => uri.AbsoluteUri,
+
+ // Throw an exception if the issuer cannot be retrieved or is not valid.
+ _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0496))
+ });
// Attach the redirect_uri to allow for later comparison when
// receiving a grant_type=authorization_code token request.
@@ -3791,7 +3742,13 @@ public async ValueTask HandleAsync(ProcessSignInContext context)
}
// Use the server identity as the token issuer.
- principal.SetClaim(Claims.Private.Issuer, (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri);
+ principal.SetClaim(Claims.Private.Issuer, (context.Options.Issuer ?? context.BaseUri) switch
+ {
+ { IsAbsoluteUri: true } uri => uri.AbsoluteUri,
+
+ // Throw an exception if the issuer cannot be retrieved or is not valid.
+ _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0496))
+ });
// Restore the device code internal token identifier from the principal
// resolved from the user code used in the end-user verification request.
@@ -4048,7 +4005,13 @@ public async ValueTask HandleAsync(ProcessSignInContext context)
}
// Use the server identity as the token issuer.
- principal.SetClaim(Claims.Private.Issuer, (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri);
+ principal.SetClaim(Claims.Private.Issuer, (context.Options.Issuer ?? context.BaseUri) switch
+ {
+ { IsAbsoluteUri: true } uri => uri.AbsoluteUri,
+
+ // Throw an exception if the issuer cannot be retrieved or is not valid.
+ _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0496))
+ });
// Set the audiences based on the resource claims stored in the principal.
principal.SetAudiences(context.Principal.GetResources());
@@ -4170,7 +4133,13 @@ public async ValueTask HandleAsync(ProcessSignInContext context)
}
// Use the server identity as the token issuer.
- principal.SetClaim(Claims.Private.Issuer, (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri);
+ principal.SetClaim(Claims.Private.Issuer, (context.Options.Issuer ?? context.BaseUri) switch
+ {
+ { IsAbsoluteUri: true } uri => uri.AbsoluteUri,
+
+ // Throw an exception if the issuer cannot be retrieved or is not valid.
+ _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0496))
+ });
// Store the type of the request token.
principal.SetClaim(Claims.Private.RequestTokenType, context.EndpointType switch
@@ -4319,7 +4288,13 @@ public async ValueTask HandleAsync(ProcessSignInContext context)
}
// Use the server identity as the token issuer.
- principal.SetClaim(Claims.Private.Issuer, (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri);
+ principal.SetClaim(Claims.Private.Issuer, (context.Options.Issuer ?? context.BaseUri) switch
+ {
+ { IsAbsoluteUri: true } uri => uri.AbsoluteUri,
+
+ // Throw an exception if the issuer cannot be retrieved or is not valid.
+ _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0496))
+ });
context.RefreshTokenPrincipal = principal;
}
@@ -4452,7 +4427,13 @@ public async ValueTask HandleAsync(ProcessSignInContext context)
}
// Use the server identity as the token issuer.
- principal.SetClaim(Claims.Private.Issuer, (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri);
+ principal.SetClaim(Claims.Private.Issuer, (context.Options.Issuer ?? context.BaseUri) switch
+ {
+ { IsAbsoluteUri: true } uri => uri.AbsoluteUri,
+
+ // Throw an exception if the issuer cannot be retrieved or is not valid.
+ _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0496))
+ });
// If available, use the client_id as both the audience and the authorized party.
// See https://openid.net/specs/openid-connect-core-1_0.html#IDToken for more information.
@@ -4579,7 +4560,13 @@ public async ValueTask HandleAsync(ProcessSignInContext context)
}
// Use the server identity as the token issuer.
- principal.SetClaim(Claims.Private.Issuer, (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri);
+ principal.SetClaim(Claims.Private.Issuer, (context.Options.Issuer ?? context.BaseUri) switch
+ {
+ { IsAbsoluteUri: true } uri => uri.AbsoluteUri,
+
+ // Throw an exception if the issuer cannot be retrieved or is not valid.
+ _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0496))
+ });
// Store the client_id as a public client_id claim.
principal.SetClaim(Claims.ClientId, context.Request.ClientId);
diff --git a/src/OpenIddict.Server/OpenIddictServerOptions.cs b/src/OpenIddict.Server/OpenIddictServerOptions.cs
index 445c4be9c..983b1681a 100644
--- a/src/OpenIddict.Server/OpenIddictServerOptions.cs
+++ b/src/OpenIddict.Server/OpenIddictServerOptions.cs
@@ -148,7 +148,7 @@ public sealed class OpenIddictServerOptions
type = usage switch
{
"access_token" => JsonWebTokenTypes.AccessToken,
- "id_token" => JsonWebTokenTypes.Jwt,
+ "id_token" => JsonWebTokenTypes.GenericJsonWebToken,
_ => throw new NotSupportedException(SR.GetResourceString(SR.ID0269))
};
diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs
index 91ba17c0a..57cca2a13 100644
--- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs
+++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs
@@ -1047,8 +1047,11 @@ public ValueTask HandleAsync(GenerateTokenContext context)
{
null or { Length: 0 } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0025)),
- // For client assertions, use the generic "JWT" type.
- TokenTypeIdentifiers.Private.ClientAssertion => JsonWebTokenTypes.Jwt,
+ // Note: OpenIddict 7.0 and higher no uses the generic "JWT" value for client assertions
+ // but uses the new standard "client-authentication+jwt" type instead, as defined in the
+ // https://www.ietf.org/archive/id/draft-ietf-oauth-rfc7523bis-01.html#name-updates-to-rfc-7523
+ // specification.
+ TokenTypeIdentifiers.Private.ClientAssertion => JsonWebTokenTypes.ClientAuthentication,
string value => value
};
diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs
index 86eeeab96..6d5d89532 100644
--- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs
+++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs
@@ -437,8 +437,12 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context)
principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value);
}
- // Use the issuer URI as the audience. Applications that need to
- // use a different value can register a custom event handler.
+ // Important: the initial Assertion Framework for OAuth 2.0 Client Authentication specifications
+ // initially encouraged supporting using the token endpoint URI as the client assertion audience,
+ // even for introspection requests. It was determined in 2025 that doing so may result in
+ // impersonation attacks as the token endpoint URI is not a guarded value. To mitigate that,
+ // OpenIddict always uses the issuer identity as the client assertion audience, as recommended
+ // by the https://www.ietf.org/archive/id/draft-ietf-oauth-rfc7523bis-01.html#section-2 draft.
principal.SetAudiences(context.Configuration.Issuer.OriginalString);
// Use the client_id as both the subject and the issuer, as required by the specifications.
diff --git a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs
index c964f2bf8..687d12d38 100644
--- a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs
+++ b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs
@@ -179,7 +179,7 @@ public sealed class OpenIddictValidationOptions
type = usage switch
{
"access_token" => JsonWebTokenTypes.AccessToken,
- "id_token" => JsonWebTokenTypes.Jwt,
+ "id_token" => JsonWebTokenTypes.GenericJsonWebToken,
_ => throw new NotSupportedException(SR.GetResourceString(SR.ID0269))
};