Browse Source

Stop using the token endpoint URI as the client assertion audience and use the new "client-authentication+jwt" JSON Web Token type

pull/2341/head
Kévin Chalet 8 months ago
parent
commit
d95b32221c
  1. 4
      src/OpenIddict.Abstractions/OpenIddictConstants.cs
  2. 3
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  3. 11
      src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs
  4. 27
      src/OpenIddict.Client/OpenIddictClientHandlers.cs
  5. 14
      src/OpenIddict.Client/OpenIddictClientOptions.cs
  6. 14
      src/OpenIddict.Client/OpenIddictClientRegistration.cs
  7. 8
      src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs
  8. 8
      src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs
  9. 8
      src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs
  10. 57
      src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs
  11. 9
      src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs
  12. 177
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  13. 2
      src/OpenIddict.Server/OpenIddictServerOptions.cs
  14. 7
      src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs
  15. 8
      src/OpenIddict.Validation/OpenIddictValidationHandlers.cs
  16. 2
      src/OpenIddict.Validation/OpenIddictValidationOptions.cs

4
src/OpenIddict.Abstractions/OpenIddictConstants.cs

@ -261,7 +261,9 @@ public static class OpenIddictConstants
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
{

3
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -1796,6 +1796,9 @@ Alternatively, any value respecting the '[region]-[subregion]-[identifier]' patt
<data name="ID0495" xml:space="preserve">
<value>The '{0}' parameter cannot contain values that are not valid absolute URIs containing no fragment component.</value>
</data>
<data name="ID0496" xml:space="preserve">
<value>The issuer cannot be retrieved from the server options or inferred from the current request or is not a valid value.</value>
</data>
<data name="ID2000" xml:space="preserve">
<value>The security token is missing.</value>
</data>

11
src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs

@ -80,7 +80,8 @@ public static partial class OpenIddictClientHandlers
//
// 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 static partial class OpenIddictClientHandlers
{
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

27
src/OpenIddict.Client/OpenIddictClientHandlers.cs

@ -2646,21 +2646,20 @@ public static partial class OpenIddictClientHandlers
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.
//

14
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

14
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

8
src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs

@ -900,7 +900,7 @@ public static partial class OpenIddictServerHandlers
// 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 static partial class OpenIddictServerHandlers
{
{ 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 static partial class OpenIddictServerHandlers
// 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);

8
src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs

@ -343,7 +343,13 @@ public static partial class OpenIddictServerHandlers
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;
}

8
src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs

@ -737,7 +737,13 @@ public static partial class OpenIddictServerHandlers
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);

57
src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs

@ -95,7 +95,7 @@ public static partial class OpenIddictServerHandlers
// 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 @@ public static partial class OpenIddictServerHandlers
// 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 @@ public static partial class OpenIddictServerHandlers
// 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 @@ public static partial class OpenIddictServerHandlers
// 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 @@ public static partial class OpenIddictServerHandlers
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,

9
src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs

@ -499,7 +499,14 @@ public static partial class OpenIddictServerHandlers
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

177
src/OpenIddict.Server/OpenIddictServerHandlers.cs

@ -758,7 +758,7 @@ public static partial class OpenIddictServerHandlers
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 static partial class OpenIddictServerHandlers
static bool ValidateClaimGroup(string name, List<Claim> 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 static partial class OpenIddictServerHandlers
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 static partial class OpenIddictServerHandlers
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 static partial class OpenIddictServerHandlers
return default;
bool ValidateAudiences(ImmutableArray<string> 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<Uri> 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 static partial class OpenIddictServerHandlers
}
// 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 static partial class OpenIddictServerHandlers
}
// 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 static partial class OpenIddictServerHandlers
}
// 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 static partial class OpenIddictServerHandlers
}
// 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 static partial class OpenIddictServerHandlers
}
// 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 static partial class OpenIddictServerHandlers
}
// 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 static partial class OpenIddictServerHandlers
}
// 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 static partial class OpenIddictServerHandlers
}
// 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);

2
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))
};

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

@ -1047,8 +1047,11 @@ public static partial class OpenIddictValidationHandlers
{
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
};

8
src/OpenIddict.Validation/OpenIddictValidationHandlers.cs

@ -437,8 +437,12 @@ public static partial class OpenIddictValidationHandlers
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.

2
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))
};

Loading…
Cancel
Save