diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/model/RegisteredClient.java b/openid-connect-common/src/main/java/org/mitre/oauth2/model/RegisteredClient.java index 4de3f5027..8e04b2a92 100644 --- a/openid-connect-common/src/main/java/org/mitre/oauth2/model/RegisteredClient.java +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/model/RegisteredClient.java @@ -31,6 +31,7 @@ import org.springframework.security.core.GrantedAuthority; import com.nimbusds.jose.EncryptionMethod; import com.nimbusds.jose.JWEAlgorithm; import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.JWKSet; /** * @author jricher @@ -460,6 +461,22 @@ public class RegisteredClient { public void setJwksUri(String jwksUri) { client.setJwksUri(jwksUri); } + /** + * @return + * @see org.mitre.oauth2.model.ClientDetailsEntity#getJwks() + */ + public JWKSet getJwks() { + return client.getJwks(); + } + + /** + * @param jwks + * @see org.mitre.oauth2.model.ClientDetailsEntity#setJwks(com.nimbusds.jose.jwk.JWKSet) + */ + public void setJwks(JWKSet jwks) { + client.setJwks(jwks); + } + /** * @return * @see org.mitre.oauth2.model.ClientDetailsEntity#getSectorIdentifierUri() diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/model/RegisteredClientFields.java b/openid-connect-common/src/main/java/org/mitre/oauth2/model/RegisteredClientFields.java index 639ac3935..cf803d5b9 100644 --- a/openid-connect-common/src/main/java/org/mitre/oauth2/model/RegisteredClientFields.java +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/model/RegisteredClientFields.java @@ -23,6 +23,7 @@ public interface RegisteredClientFields { public String SECTOR_IDENTIFIER_URI = "sector_identifier_uri"; public String APPLICATION_TYPE = "application_type"; public String JWKS_URI = "jwks_uri"; + public String JWKS = "jwks"; public String SCOPE_SEPARATOR = " "; public String POLICY_URI = "policy_uri"; public String RESPONSE_TYPES = "response_types"; diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/ClientDetailsEntityJsonProcessor.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/ClientDetailsEntityJsonProcessor.java index ff4370ec8..5bdaa42f0 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/ClientDetailsEntityJsonProcessor.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/ClientDetailsEntityJsonProcessor.java @@ -20,11 +20,15 @@ package org.mitre.openid.connect; +import java.text.ParseException; + import org.mitre.oauth2.model.ClientDetailsEntity; import org.mitre.oauth2.model.ClientDetailsEntity.AppType; import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod; import org.mitre.oauth2.model.ClientDetailsEntity.SubjectType; import org.mitre.oauth2.model.RegisteredClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.google.common.base.Joiner; import com.google.common.base.Splitter; @@ -32,6 +36,7 @@ import com.google.common.collect.Sets; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import com.nimbusds.jose.jwk.JWKSet; import static org.mitre.oauth2.model.RegisteredClientFields.APPLICATION_TYPE; import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_ID; @@ -49,6 +54,7 @@ import static org.mitre.oauth2.model.RegisteredClientFields.ID_TOKEN_ENCRYPTED_R import static org.mitre.oauth2.model.RegisteredClientFields.ID_TOKEN_SIGNED_RESPONSE_ALG; import static org.mitre.oauth2.model.RegisteredClientFields.INITIATE_LOGIN_URI; import static org.mitre.oauth2.model.RegisteredClientFields.JWKS_URI; +import static org.mitre.oauth2.model.RegisteredClientFields.JWKS; import static org.mitre.oauth2.model.RegisteredClientFields.LOGO_URI; import static org.mitre.oauth2.model.RegisteredClientFields.POLICY_URI; import static org.mitre.oauth2.model.RegisteredClientFields.POST_LOGOUT_REDIRECT_URIS; @@ -78,11 +84,14 @@ import static org.mitre.util.JsonUtils.getAsString; import static org.mitre.util.JsonUtils.getAsStringSet; /** + * Utility class to handle the parsing and serialization of ClientDetails objects. + * * @author jricher * */ public class ClientDetailsEntityJsonProcessor { + private static Logger logger = LoggerFactory.getLogger(ClientDetailsEntityJsonProcessor.class); private static JsonParser parser = new JsonParser(); @@ -104,8 +113,6 @@ public class ClientDetailsEntityJsonProcessor { JsonObject o = jsonEl.getAsJsonObject(); ClientDetailsEntity c = new ClientDetailsEntity(); - // TODO: make these field names into constants - // these two fields should only be sent in the update request, and MUST match existing values c.setClientId(getAsString(o, CLIENT_ID)); c.setClientSecret(getAsString(o, CLIENT_SECRET)); @@ -133,7 +140,16 @@ public class ClientDetailsEntityJsonProcessor { c.setResponseTypes(getAsStringSet(o, RESPONSE_TYPES)); c.setPolicyUri(getAsString(o, POLICY_URI)); c.setJwksUri(getAsString(o, JWKS_URI)); - + + JsonElement jwksEl = o.get(JWKS); + if (jwksEl != null && jwksEl.isJsonObject()) { + try { + JWKSet jwks = JWKSet.parse(jwksEl.toString()); // we have to pass this through Nimbus's parser as a string + c.setJwks(jwks); + } catch (ParseException e) { + logger.error("Unable to parse JWK Set for client", e); + } + } // OIDC Additions String appType = getAsString(o, APPLICATION_TYPE); @@ -261,6 +277,15 @@ public class ClientDetailsEntityJsonProcessor { o.add(RESPONSE_TYPES, getAsArray(c.getResponseTypes())); o.addProperty(POLICY_URI, c.getPolicyUri()); o.addProperty(JWKS_URI, c.getJwksUri()); + + // get the JWKS sub-object + if (c.getJwks() != null) { + // We have to re-parse it into GSON because Nimbus uses a different parser + JsonElement jwks = parser.parse(c.getJwks().toString()); + o.add(JWKS, jwks); + } else { + o.add(JWKS, null); + } // OIDC Registration o.addProperty(APPLICATION_TYPE, c.getApplicationType() != null ? c.getApplicationType().getValue() : null); diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultOAuth2ClientDetailsEntityService.java b/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultOAuth2ClientDetailsEntityService.java index da2a27b67..284b4a7d4 100644 --- a/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultOAuth2ClientDetailsEntityService.java +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultOAuth2ClientDetailsEntityService.java @@ -114,6 +114,9 @@ public class DefaultOAuth2ClientDetailsEntityService implements ClientDetailsEnt // make sure that clients with the "refresh_token" grant type have the "offline_access" scope, and vice versa ensureRefreshTokenConsistency(client); + + // make sure we don't have both a JWKS and a JWKS URI + ensureKeyConsistency(client); // timestamp this to right now client.setCreatedAt(new Date()); @@ -132,6 +135,16 @@ public class DefaultOAuth2ClientDetailsEntityService implements ClientDetailsEnt return c; } + /** + * @param client + */ + private void ensureKeyConsistency(ClientDetailsEntity client) { + if (client.getJwksUri() != null && client.getJwks() != null) { + // a client can only have one key type or the other, not both + throw new IllegalArgumentException("A client cannot have both JWKS URI and JWKS value"); + } + } + private void ensureNoReservedScopes(ClientDetailsEntity client) { // make sure a client doesn't get any special system scopes Set requestedScope = scopeService.fromStrings(client.getScope()); @@ -252,6 +265,9 @@ public class DefaultOAuth2ClientDetailsEntityService implements ClientDetailsEnt // if the client is flagged to allow for refresh tokens, make sure it's got the right scope ensureRefreshTokenConsistency(newClient); + // make sure we don't have both a JWKS and a JWKS URI + ensureKeyConsistency(newClient); + // check the sector URI checkSectorIdentifierUri(newClient); diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/request/ConnectOAuth2RequestFactory.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/request/ConnectOAuth2RequestFactory.java index be4685826..1e93765de 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/request/ConnectOAuth2RequestFactory.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/request/ConnectOAuth2RequestFactory.java @@ -217,7 +217,7 @@ public class ConnectOAuth2RequestFactory extends DefaultOAuth2RequestFactory { // it's a public key, need to find the JWK URI and fetch the key - if (client.getJwksUri() == null) { + if (Strings.isNullOrEmpty(client.getJwksUri()) && client.getJwks() == null) { throw new InvalidClientException("Client must have a JWKS registered to use signed request objects with a public key."); } diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultOIDCTokenService.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultOIDCTokenService.java index d2e7a5469..e0d736f0b 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultOIDCTokenService.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/DefaultOIDCTokenService.java @@ -142,7 +142,7 @@ public class DefaultOIDCTokenService implements OIDCTokenService { if (client.getIdTokenEncryptedResponseAlg() != null && !client.getIdTokenEncryptedResponseAlg().equals(Algorithm.NONE) && client.getIdTokenEncryptedResponseEnc() != null && !client.getIdTokenEncryptedResponseEnc().equals(Algorithm.NONE) - && !Strings.isNullOrEmpty(client.getJwksUri())) { + && (!Strings.isNullOrEmpty(client.getJwksUri()) || client.getJwks() != null)) { JWTEncryptionAndDecryptionService encrypter = encrypters.getEncrypter(client.getJwksUri()); diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/MITREidDataService_1_0.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/MITREidDataService_1_0.java index 2c37b302f..2e0fe9b4d 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/MITREidDataService_1_0.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/MITREidDataService_1_0.java @@ -720,6 +720,8 @@ public class MITREidDataService_1_0 extends MITREidDataServiceSupport implements } else if (name.equals("subjectType")) { SubjectType st = SubjectType.getByValue(reader.nextString()); client.setSubjectType(st); + } else if (name.equals("jwks_uri")) { + client.setJwksUri(reader.nextString()); } else if (name.equals("requestObjectSigningAlg")) { JWSAlgorithm alg = JWSAlgorithm.parse(reader.nextString()); client.setRequestObjectSigningAlg(alg); diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/MITREidDataService_1_1.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/MITREidDataService_1_1.java index c34757abd..26f5d2cad 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/MITREidDataService_1_1.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/MITREidDataService_1_1.java @@ -730,6 +730,8 @@ public class MITREidDataService_1_1 extends MITREidDataServiceSupport implements } else if (name.equals("subjectType")) { SubjectType st = SubjectType.getByValue(reader.nextString()); client.setSubjectType(st); + } else if (name.equals("jwks_uri")) { + client.setJwksUri(reader.nextString()); } else if (name.equals("requestObjectSigningAlg")) { JWSAlgorithm alg = JWSAlgorithm.parse(reader.nextString()); client.setRequestObjectSigningAlg(alg); diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/MITREidDataService_1_2.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/MITREidDataService_1_2.java index 7ac94f0b6..b77ba17f3 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/MITREidDataService_1_2.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/service/impl/MITREidDataService_1_2.java @@ -59,6 +59,7 @@ import com.google.gson.stream.JsonWriter; import com.nimbusds.jose.EncryptionMethod; import com.nimbusds.jose.JWEAlgorithm; import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jwt.JWTParser; import static org.mitre.util.JsonUtils.readMap; @@ -387,6 +388,7 @@ public class MITREidDataService_1_2 extends MITREidDataServiceSupport implements writer.endArray(); writer.name("policyUri").value(client.getPolicyUri()); writer.name("jwksUri").value(client.getJwksUri()); + writer.name("jwks").value((client.getJwks() != null) ? client.getJwks().toString() : null); writer.name("applicationType") .value((client.getApplicationType() != null) ? client.getApplicationType().getValue() : null); writer.name("sectorIdentifierUri").value(client.getSectorIdentifierUri()); @@ -1001,6 +1003,14 @@ public class MITREidDataService_1_2 extends MITREidDataServiceSupport implements } else if (name.equals("subjectType")) { SubjectType st = SubjectType.getByValue(reader.nextString()); client.setSubjectType(st); + } else if (name.equals("jwks_uri")) { + client.setJwksUri(reader.nextString()); + } else if (name.equals("jwks")) { + try { + client.setJwks(JWKSet.parse(reader.nextString())); + } catch (ParseException e) { + logger.error("Couldn't parse JWK Set", e); + } } else if (name.equals("requestObjectSigningAlg")) { JWSAlgorithm alg = JWSAlgorithm.parse(reader.nextString()); client.setRequestObjectSigningAlg(alg); diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/view/AbstractClientEntityView.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/view/AbstractClientEntityView.java index 923839ea5..fd8d2a608 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/view/AbstractClientEntityView.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/view/AbstractClientEntityView.java @@ -37,12 +37,14 @@ import com.google.gson.ExclusionStrategy; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; +import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; import com.nimbusds.jose.EncryptionMethod; import com.nimbusds.jose.JWEAlgorithm; import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.JWKSet; /** * @@ -60,6 +62,8 @@ public abstract class AbstractClientEntityView extends AbstractView { */ private static final Logger logger = LoggerFactory.getLogger(AbstractClientEntityView.class); + private JsonParser parser = new JsonParser(); + private Gson gson = new GsonBuilder() .setExclusionStrategies(getExclusionStrategy()) .registerTypeAdapter(JWSAlgorithm.class, new JsonSerializer() { @@ -92,6 +96,16 @@ public abstract class AbstractClientEntityView extends AbstractView { } } }) + .registerTypeAdapter(JWKSet.class, new JsonSerializer() { + @Override + public JsonElement serialize(JWKSet src, Type typeOfSrc, JsonSerializationContext context) { + if (src != null) { + return parser.parse(src.toString()); + } else { + return null; + } + } + }) .serializeNulls() .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") .create(); diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/view/UserInfoJWTView.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/view/UserInfoJWTView.java index a918dfeb3..1ebfb5a02 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/view/UserInfoJWTView.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/view/UserInfoJWTView.java @@ -111,7 +111,7 @@ public class UserInfoJWTView extends UserInfoView { if (client.getIdTokenEncryptedResponseAlg() != null && !client.getIdTokenEncryptedResponseAlg().equals(Algorithm.NONE) && client.getIdTokenEncryptedResponseEnc() != null && !client.getIdTokenEncryptedResponseEnc().equals(Algorithm.NONE) - && !Strings.isNullOrEmpty(client.getJwksUri())) { + && (!Strings.isNullOrEmpty(client.getJwksUri()) || client.getJwks() != null)) { // encrypt it to the client's key diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ClientAPI.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ClientAPI.java index a70ea964b..149da9826 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ClientAPI.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ClientAPI.java @@ -17,6 +17,7 @@ package org.mitre.openid.connect.web; import java.lang.reflect.Type; +import java.text.ParseException; import java.util.Collection; import org.mitre.oauth2.model.ClientDetailsEntity; @@ -62,6 +63,7 @@ import com.nimbusds.jose.Algorithm; import com.nimbusds.jose.EncryptionMethod; import com.nimbusds.jose.JWEAlgorithm; import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.JWKSet; /** * @author Michael Jett @@ -117,6 +119,20 @@ public class ClientAPI { } } }) + .registerTypeAdapter(JWKSet.class, new JsonDeserializer() { + @Override + public JWKSet deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + if (json.isJsonObject()) { + try { + return JWKSet.parse(json.toString()); + } catch (ParseException e) { + return null; + } + } else { + return null; + } + } + }) .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") .create(); @@ -196,7 +212,7 @@ public class ClientAPI { } else if (client.getTokenEndpointAuthMethod().equals(AuthMethod.PRIVATE_KEY)) { - if (Strings.isNullOrEmpty(client.getJwksUri())) { + if (Strings.isNullOrEmpty(client.getJwksUri()) && client.getJwks() == null) { logger.error("tried to create client with private key auth but no private key"); m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Can not create a client with private key authentication without registering a key via the JWS Set URI."); @@ -215,16 +231,23 @@ public class ClientAPI { } - + client.setDynamicallyRegistered(false); - ClientDetailsEntity newClient = clientService.saveNewClient(client); - m.addAttribute(JsonEntityView.ENTITY, newClient); - - if (AuthenticationUtilities.isAdmin(auth)) { - return ClientEntityViewForAdmins.VIEWNAME; - } else { - return ClientEntityViewForUsers.VIEWNAME; + try { + ClientDetailsEntity newClient = clientService.saveNewClient(client); + m.addAttribute(JsonEntityView.ENTITY, newClient); + + if (AuthenticationUtilities.isAdmin(auth)) { + return ClientEntityViewForAdmins.VIEWNAME; + } else { + return ClientEntityViewForUsers.VIEWNAME; + } + } catch (IllegalArgumentException e) { + logger.error("Unable to save client: {}", e.getMessage()); + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unable to save client"); + return JsonErrorView.VIEWNAME; } } @@ -292,7 +315,7 @@ public class ClientAPI { } else if (client.getTokenEndpointAuthMethod().equals(AuthMethod.PRIVATE_KEY)) { - if (Strings.isNullOrEmpty(client.getJwksUri())) { + if (Strings.isNullOrEmpty(client.getJwksUri()) && client.getJwks() != null) { logger.error("tried to create client with private key auth but no private key"); m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Can not create a client with private key authentication without registering a key via the JWS Set URI."); @@ -312,13 +335,20 @@ public class ClientAPI { } - ClientDetailsEntity newClient = clientService.updateClient(oldClient, client); - m.addAttribute(JsonEntityView.ENTITY, newClient); - - if (AuthenticationUtilities.isAdmin(auth)) { - return ClientEntityViewForAdmins.VIEWNAME; - } else { - return ClientEntityViewForUsers.VIEWNAME; + try { + ClientDetailsEntity newClient = clientService.updateClient(oldClient, client); + m.addAttribute(JsonEntityView.ENTITY, newClient); + + if (AuthenticationUtilities.isAdmin(auth)) { + return ClientEntityViewForAdmins.VIEWNAME; + } else { + return ClientEntityViewForUsers.VIEWNAME; + } + } catch (IllegalArgumentException e) { + logger.error("Unable to save client: {}", e.getMessage()); + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unable to save client"); + return JsonErrorView.VIEWNAME; } } diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/DynamicClientRegistrationEndpoint.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/DynamicClientRegistrationEndpoint.java index e6672de8c..39b880e0f 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/DynamicClientRegistrationEndpoint.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/DynamicClientRegistrationEndpoint.java @@ -527,7 +527,7 @@ public class DynamicClientRegistrationEndpoint { newClient = clientService.generateClientSecret(newClient); } } else if (newClient.getTokenEndpointAuthMethod() == AuthMethod.PRIVATE_KEY) { - if (Strings.isNullOrEmpty(newClient.getJwksUri())) { + if (Strings.isNullOrEmpty(newClient.getJwksUri()) && newClient.getJwks() == null) { throw new ValidationException("invalid_client_metadata", "JWK Set URI required when using private key authentication", HttpStatus.BAD_REQUEST); } diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ProtectedResourceRegistrationEndpoint.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ProtectedResourceRegistrationEndpoint.java index 340e18ccd..d081b3a25 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ProtectedResourceRegistrationEndpoint.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ProtectedResourceRegistrationEndpoint.java @@ -440,7 +440,7 @@ public class ProtectedResourceRegistrationEndpoint { newClient = clientService.generateClientSecret(newClient); } } else if (newClient.getTokenEndpointAuthMethod() == AuthMethod.PRIVATE_KEY) { - if (Strings.isNullOrEmpty(newClient.getJwksUri())) { + if (Strings.isNullOrEmpty(newClient.getJwksUri()) && newClient.getJwks() == null) { throw new ValidationException("invalid_client_metadata", "JWK Set URI required when using private key authentication", HttpStatus.BAD_REQUEST); }