added JWKs-by-value support to client data model and API, closes #826

pull/834/head
Justin Richer 10 years ago
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…
Cancel
Save