diff --git a/identity-server/src/IdentityServer/Configuration/DependencyInjection/Options/IdentityServerOptions.cs b/identity-server/src/IdentityServer/Configuration/DependencyInjection/Options/IdentityServerOptions.cs
index c4f1692dd..bf616e0ec 100644
--- a/identity-server/src/IdentityServer/Configuration/DependencyInjection/Options/IdentityServerOptions.cs
+++ b/identity-server/src/IdentityServer/Configuration/DependencyInjection/Options/IdentityServerOptions.cs
@@ -69,6 +69,11 @@ public class IdentityServerOptions
///
public bool StrictJarValidation { get; set; } = false;
+ ///
+ /// When clients authenticate with private_key_jwt assertions, validate the audience of the assertion strictly: the audience must be this IdentityServer's issuer identifier as a single string.
+ ///
+ public bool StrictClientAssertionAudienceValidation { get; set; } = false;
+
///
/// Specifies if a user's tenant claim is compared to the tenant acr_values parameter value to determine if the login page is displayed. Defaults to false.
///
diff --git a/identity-server/src/IdentityServer/Validation/Default/PrivateKeyJwtSecretValidator.cs b/identity-server/src/IdentityServer/Validation/Default/PrivateKeyJwtSecretValidator.cs
index afe93fc99..6fe43daf4 100644
--- a/identity-server/src/IdentityServer/Validation/Default/PrivateKeyJwtSecretValidator.cs
+++ b/identity-server/src/IdentityServer/Validation/Default/PrivateKeyJwtSecretValidator.cs
@@ -89,20 +89,7 @@ public async Task ValidateAsync(IEnumerable secr
return fail;
}
- var validAudiences = new[]
- {
- // token endpoint URL
- string.Concat(_urls.BaseUrl.EnsureTrailingSlash(), ProtocolRoutePaths.Token),
- // issuer URL + token (legacy support)
- string.Concat((await _issuerNameService.GetCurrentAsync()).EnsureTrailingSlash(), ProtocolRoutePaths.Token),
- // issuer URL
- await _issuerNameService.GetCurrentAsync(),
- // CIBA endpoint: https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0.html#auth_request
- string.Concat(_urls.BaseUrl.EnsureTrailingSlash(), ProtocolRoutePaths.BackchannelAuthentication),
- // PAR endpoint: https://datatracker.ietf.org/doc/html/rfc9126#name-request
- string.Concat(_urls.BaseUrl.EnsureTrailingSlash(), ProtocolRoutePaths.PushedAuthorization),
-
- }.Distinct();
+ var issuer = await _issuerNameService.GetCurrentAsync();
var tokenValidationParameters = new TokenValidationParameters
{
@@ -111,15 +98,46 @@ await _issuerNameService.GetCurrentAsync(),
ValidIssuer = parsedSecret.Id,
ValidateIssuer = true,
-
- ValidAudiences = validAudiences,
- ValidateAudience = true,
-
+
RequireSignedTokens = true,
RequireExpirationTime = true,
ClockSkew = TimeSpan.FromMinutes(5)
};
+
+ if (_options.StrictClientAssertionAudienceValidation)
+ {
+ // New strict audience validation requires that the audience be the issuer identifier, disallows multiple
+ // audiences in an array, and even disallows wrapping even a single audience in an array
+ tokenValidationParameters.AudienceValidator = (audiences, token, parameters) =>
+ {
+ // There isn't a particularly nice way to distinguish between a claim that is a single string wrapped in
+ // an array and just a single string when using a JsonWebToken. The jwt.GetClaim function and jwt.Claims
+ // collection both convert that into a string valued claim. However, GetPayloadValue