added JWKs-by-value support to client data model and API, closes #826
parent
30162f6baa
commit
032d41e5ed
|
@ -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()
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<SystemScope> 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);
|
||||
|
||||
|
|
|
@ -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.");
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<JWSAlgorithm>() {
|
||||
|
@ -92,6 +96,16 @@ public abstract class AbstractClientEntityView extends AbstractView {
|
|||
}
|
||||
}
|
||||
})
|
||||
.registerTypeAdapter(JWKSet.class, new JsonSerializer<JWKSet>() {
|
||||
@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();
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 <mjett@mitre.org>
|
||||
|
@ -117,6 +119,20 @@ public class ClientAPI {
|
|||
}
|
||||
}
|
||||
})
|
||||
.registerTypeAdapter(JWKSet.class, new JsonDeserializer<JWKSet>() {
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue