From 57208ac35db51c7fbe3fecc199106864f9b04f91 Mon Sep 17 00:00:00 2001 From: Justin Richer Date: Sun, 24 Jul 2016 16:12:56 -0400 Subject: [PATCH] added software statements to client API --- .../main/webapp/WEB-INF/assertion-config.xml | 17 +- .../src/main/webapp/resources/js/client.js | 1 + .../resources/js/locale/en/messages.json | 3 + .../webapp/resources/template/client.html | 10 + .../view/AbstractClientEntityView.java | 12 + .../mitre/openid/connect/web/ClientAPI.java | 227 +++++++++++++++++- 6 files changed, 258 insertions(+), 12 deletions(-) diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/assertion-config.xml b/openid-connect-server-webapp/src/main/webapp/WEB-INF/assertion-config.xml index a7df61579..3a7c4fb47 100644 --- a/openid-connect-server-webapp/src/main/webapp/WEB-INF/assertion-config.xml +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/assertion-config.xml @@ -32,19 +32,20 @@ + - + - - - - - - - + + + + + + + \ No newline at end of file diff --git a/openid-connect-server-webapp/src/main/webapp/resources/js/client.js b/openid-connect-server-webapp/src/main/webapp/resources/js/client.js index 3e53c7e19..b33b7b4c1 100644 --- a/openid-connect-server-webapp/src/main/webapp/resources/js/client.js +++ b/openid-connect-server-webapp/src/main/webapp/resources/js/client.js @@ -936,6 +936,7 @@ var ClientFormView = Backbone.View.extend({ jwksUri: jwksUri, jwks: jwks, subjectType: subjectType, + softwareStatement: $('#softwareStatement textarea').val(), tokenEndpointAuthMethod: tokenEndpointAuthMethod, responseTypes: responseTypes, sectorIdentifierUri: sectorIdentifierUri, diff --git a/openid-connect-server-webapp/src/main/webapp/resources/js/locale/en/messages.json b/openid-connect-server-webapp/src/main/webapp/resources/js/locale/en/messages.json index 98fe90b51..f787e0d54 100644 --- a/openid-connect-server-webapp/src/main/webapp/resources/js/locale/en/messages.json +++ b/openid-connect-server-webapp/src/main/webapp/resources/js/locale/en/messages.json @@ -158,6 +158,9 @@ "ps384": "RSASSA-PSS using SHA-384 and MGF1 with SHA-384", "ps512": "RSASSA-PSS using SHA-512 and MGF1 with SHA-512" }, + "software-statement": "Software Statement", + "software-statement-placeholder": "eyj0...", + "software-statement-help": "A software statement is issued by a trusted third party and locks certain elements of a client's registration", "subject-type": "Subject Type", "terms": "Terms of Service", "terms-help": "URL for the Terms of Service of this client, will be displayed to the user", diff --git a/openid-connect-server-webapp/src/main/webapp/resources/template/client.html b/openid-connect-server-webapp/src/main/webapp/resources/template/client.html index df24ab06c..710cab691 100644 --- a/openid-connect-server-webapp/src/main/webapp/resources/template/client.html +++ b/openid-connect-server-webapp/src/main/webapp/resources/template/client.html @@ -298,6 +298,16 @@
+ +
+ +
+ +

A software statement is issued by a trusted third party and locks certain elements of a client's registration

+
+
+
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 dbe9407c0..854c10847 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 @@ -45,6 +45,7 @@ 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.JWT; /** * @@ -106,6 +107,17 @@ public abstract class AbstractClientEntityView extends AbstractView { } } }) + .registerTypeAdapter(JWT.class, new JsonSerializer() { + @Override + public JsonElement serialize(JWT src, Type typeOfSrc, JsonSerializationContext context) { + if (src != null) { + return new JsonPrimitive(src.serialize()); + } 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/web/ClientAPI.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ClientAPI.java index a950c29ea..2cce0e228 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 @@ -20,13 +20,18 @@ import java.lang.reflect.Type; import java.sql.SQLIntegrityConstraintViolationException; import java.text.ParseException; import java.util.Collection; + import javax.persistence.PersistenceException; import org.eclipse.persistence.exceptions.DatabaseException; +import org.mitre.jwt.assertion.AssertionValidator; 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.service.ClientDetailsEntityService; import org.mitre.oauth2.web.AuthenticationUtilities; +import org.mitre.openid.connect.exception.ValidationException; import org.mitre.openid.connect.model.CachedImage; import org.mitre.openid.connect.service.ClientLogoLoadingService; import org.mitre.openid.connect.view.ClientEntityViewForAdmins; @@ -37,12 +42,14 @@ import org.mitre.openid.connect.view.JsonErrorView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.common.util.OAuth2Utils; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.PathVariable; @@ -52,6 +59,7 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.servlet.ModelAndView; import com.google.common.base.Strings; +import com.google.common.collect.Sets; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonDeserializationContext; @@ -60,12 +68,57 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; import com.google.gson.JsonSyntaxException; 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; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.JWTParser; + +import static org.mitre.oauth2.model.RegisteredClientFields.APPLICATION_TYPE; +import static org.mitre.oauth2.model.RegisteredClientFields.CLAIMS_REDIRECT_URIS; +import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_ID; +import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_ID_ISSUED_AT; +import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_NAME; +import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_SECRET; +import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_SECRET_EXPIRES_AT; +import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_URI; +import static org.mitre.oauth2.model.RegisteredClientFields.CONTACTS; +import static org.mitre.oauth2.model.RegisteredClientFields.DEFAULT_ACR_VALUES; +import static org.mitre.oauth2.model.RegisteredClientFields.DEFAULT_MAX_AGE; +import static org.mitre.oauth2.model.RegisteredClientFields.GRANT_TYPES; +import static org.mitre.oauth2.model.RegisteredClientFields.ID_TOKEN_ENCRYPTED_RESPONSE_ALG; +import static org.mitre.oauth2.model.RegisteredClientFields.ID_TOKEN_ENCRYPTED_RESPONSE_ENC; +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; +import static org.mitre.oauth2.model.RegisteredClientFields.JWKS_URI; +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; +import static org.mitre.oauth2.model.RegisteredClientFields.REDIRECT_URIS; +import static org.mitre.oauth2.model.RegisteredClientFields.REGISTRATION_ACCESS_TOKEN; +import static org.mitre.oauth2.model.RegisteredClientFields.REGISTRATION_CLIENT_URI; +import static org.mitre.oauth2.model.RegisteredClientFields.REQUEST_OBJECT_SIGNING_ALG; +import static org.mitre.oauth2.model.RegisteredClientFields.REQUEST_URIS; +import static org.mitre.oauth2.model.RegisteredClientFields.REQUIRE_AUTH_TIME; +import static org.mitre.oauth2.model.RegisteredClientFields.RESPONSE_TYPES; +import static org.mitre.oauth2.model.RegisteredClientFields.SCOPE; +import static org.mitre.oauth2.model.RegisteredClientFields.SECTOR_IDENTIFIER_URI; +import static org.mitre.oauth2.model.RegisteredClientFields.SOFTWARE_STATEMENT; +import static org.mitre.oauth2.model.RegisteredClientFields.SUBJECT_TYPE; +import static org.mitre.oauth2.model.RegisteredClientFields.TOKEN_ENDPOINT_AUTH_METHOD; +import static org.mitre.oauth2.model.RegisteredClientFields.TOKEN_ENDPOINT_AUTH_SIGNING_ALG; +import static org.mitre.oauth2.model.RegisteredClientFields.TOS_URI; +import static org.mitre.oauth2.model.RegisteredClientFields.USERINFO_ENCRYPTED_RESPONSE_ALG; +import static org.mitre.oauth2.model.RegisteredClientFields.USERINFO_ENCRYPTED_RESPONSE_ENC; +import static org.mitre.oauth2.model.RegisteredClientFields.USERINFO_SIGNED_RESPONSE_ALG; /** * @author Michael Jett @@ -83,6 +136,10 @@ public class ClientAPI { @Autowired private ClientLogoLoadingService clientLogoLoadingService; + + @Autowired + @Qualifier("clientAssertionValidator") + private AssertionValidator assertionValidator; private JsonParser parser = new JsonParser(); @@ -132,6 +189,20 @@ public class ClientAPI { } } }) + .registerTypeAdapter(JWT.class, new JsonDeserializer() { + @Override + public JWT deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + if (json.isJsonPrimitive()) { + try { + return JWTParser.parse(json.getAsString()); + } catch (ParseException e) { + return null; + } + } else { + return null; + } + } + }) .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") .create(); @@ -175,8 +246,8 @@ public class ClientAPI { try { json = parser.parse(jsonString).getAsJsonObject(); client = gson.fromJson(json, ClientDetailsEntity.class); - } - catch (JsonSyntaxException e) { + client = validateSoftwareStatement(client); + } catch (JsonSyntaxException e) { logger.error("apiAddClient failed due to JsonSyntaxException", e); m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Could not save new client. The server encountered a JSON syntax exception. Contact a system administrator for assistance."); @@ -186,6 +257,11 @@ public class ClientAPI { m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Could not save new client. The server encountered an IllegalStateException. Refresh and try again - if the problem persists, contact a system administrator for assistance."); return JsonErrorView.VIEWNAME; + } catch (ValidationException e) { + logger.error("apiUpdateClient failed due to ValidationException", e); + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Could not update client. The server encountered a ValidationException."); + return JsonErrorView.VIEWNAME; } // if they leave the client identifier empty, force it to be generated @@ -281,8 +357,8 @@ public class ClientAPI { // parse the client passed in (from JSON) and fetch the old client from the store json = parser.parse(jsonString).getAsJsonObject(); client = gson.fromJson(json, ClientDetailsEntity.class); - } - catch (JsonSyntaxException e) { + client = validateSoftwareStatement(client); + } catch (JsonSyntaxException e) { logger.error("apiUpdateClient failed due to JsonSyntaxException", e); m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Could not update client. The server encountered a JSON syntax exception. Contact a system administrator for assistance."); @@ -292,6 +368,11 @@ public class ClientAPI { m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Could not update client. The server encountered an IllegalStateException. Refresh and try again - if the problem persists, contact a system administrator for assistance."); return JsonErrorView.VIEWNAME; + } catch (ValidationException e) { + logger.error("apiUpdateClient failed due to ValidationException", e); + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Could not update client. The server encountered a ValidationException."); + return JsonErrorView.VIEWNAME; } ClientDetailsEntity oldClient = clientService.getClientById(id); @@ -441,4 +522,142 @@ public class ClientAPI { } } + private ClientDetailsEntity validateSoftwareStatement(ClientDetailsEntity newClient) throws ValidationException { + if (newClient.getSoftwareStatement() != null) { + if (assertionValidator.isValid(newClient.getSoftwareStatement())) { + // we have a software statement and its envelope passed all the checks from our validator + + // swap out all of the client's fields for the associated parts of the software statement + try { + JWTClaimsSet claimSet = newClient.getSoftwareStatement().getJWTClaimsSet(); + for (String claim : claimSet.getClaims().keySet()) { + switch (claim) { + case SOFTWARE_STATEMENT: + throw new ValidationException("invalid_client_metadata", "Software statement can't include another software statement", HttpStatus.BAD_REQUEST); + case CLAIMS_REDIRECT_URIS: + newClient.setClaimsRedirectUris(Sets.newHashSet(claimSet.getStringListClaim(claim))); + break; + case CLIENT_SECRET_EXPIRES_AT: + throw new ValidationException("invalid_client_metadata", "Software statement can't include a client secret expiration time", HttpStatus.BAD_REQUEST); + case CLIENT_ID_ISSUED_AT: + throw new ValidationException("invalid_client_metadata", "Software statement can't include a client ID issuance time", HttpStatus.BAD_REQUEST); + case REGISTRATION_CLIENT_URI: + throw new ValidationException("invalid_client_metadata", "Software statement can't include a client configuration endpoint", HttpStatus.BAD_REQUEST); + case REGISTRATION_ACCESS_TOKEN: + throw new ValidationException("invalid_client_metadata", "Software statement can't include a client registration access token", HttpStatus.BAD_REQUEST); + case REQUEST_URIS: + newClient.setRequestUris(Sets.newHashSet(claimSet.getStringListClaim(claim))); + break; + case POST_LOGOUT_REDIRECT_URIS: + newClient.setPostLogoutRedirectUris(Sets.newHashSet(claimSet.getStringListClaim(claim))); + break; + case INITIATE_LOGIN_URI: + newClient.setInitiateLoginUri(claimSet.getStringClaim(claim)); + break; + case DEFAULT_ACR_VALUES: + newClient.setDefaultACRvalues(Sets.newHashSet(claimSet.getStringListClaim(claim))); + break; + case REQUIRE_AUTH_TIME: + newClient.setRequireAuthTime(claimSet.getBooleanClaim(claim)); + break; + case DEFAULT_MAX_AGE: + newClient.setDefaultMaxAge(claimSet.getIntegerClaim(claim)); + break; + case TOKEN_ENDPOINT_AUTH_SIGNING_ALG: + newClient.setTokenEndpointAuthSigningAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim))); + break; + case ID_TOKEN_ENCRYPTED_RESPONSE_ENC: + newClient.setIdTokenEncryptedResponseEnc(EncryptionMethod.parse(claimSet.getStringClaim(claim))); + break; + case ID_TOKEN_ENCRYPTED_RESPONSE_ALG: + newClient.setIdTokenEncryptedResponseAlg(JWEAlgorithm.parse(claimSet.getStringClaim(claim))); + break; + case ID_TOKEN_SIGNED_RESPONSE_ALG: + newClient.setIdTokenSignedResponseAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim))); + break; + case USERINFO_ENCRYPTED_RESPONSE_ENC: + newClient.setUserInfoEncryptedResponseEnc(EncryptionMethod.parse(claimSet.getStringClaim(claim))); + break; + case USERINFO_ENCRYPTED_RESPONSE_ALG: + newClient.setUserInfoEncryptedResponseAlg(JWEAlgorithm.parse(claimSet.getStringClaim(claim))); + break; + case USERINFO_SIGNED_RESPONSE_ALG: + newClient.setUserInfoSignedResponseAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim))); + break; + case REQUEST_OBJECT_SIGNING_ALG: + newClient.setRequestObjectSigningAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim))); + break; + case SUBJECT_TYPE: + newClient.setSubjectType(SubjectType.getByValue(claimSet.getStringClaim(claim))); + break; + case SECTOR_IDENTIFIER_URI: + newClient.setSectorIdentifierUri(claimSet.getStringClaim(claim)); + break; + case APPLICATION_TYPE: + newClient.setApplicationType(AppType.getByValue(claimSet.getStringClaim(claim))); + break; + case JWKS_URI: + newClient.setJwksUri(claimSet.getStringClaim(claim)); + break; + case JWKS: + newClient.setJwks(JWKSet.parse(claimSet.getStringClaim(claim))); + break; + case POLICY_URI: + newClient.setPolicyUri(claimSet.getStringClaim(claim)); + break; + case RESPONSE_TYPES: + newClient.setResponseTypes(Sets.newHashSet(claimSet.getStringListClaim(claim))); + break; + case GRANT_TYPES: + newClient.setGrantTypes(Sets.newHashSet(claimSet.getStringListClaim(claim))); + break; + case SCOPE: + newClient.setScope(OAuth2Utils.parseParameterList(claimSet.getStringClaim(claim))); + break; + case TOKEN_ENDPOINT_AUTH_METHOD: + newClient.setTokenEndpointAuthMethod(AuthMethod.getByValue(claimSet.getStringClaim(claim))); + break; + case TOS_URI: + newClient.setTosUri(claimSet.getStringClaim(claim)); + break; + case CONTACTS: + newClient.setContacts(Sets.newHashSet(claimSet.getStringListClaim(claim))); + break; + case LOGO_URI: + newClient.setLogoUri(claimSet.getStringClaim(claim)); + break; + case CLIENT_URI: + newClient.setClientUri(claimSet.getStringClaim(claim)); + break; + case CLIENT_NAME: + newClient.setClientName(claimSet.getStringClaim(claim)); + break; + case REDIRECT_URIS: + newClient.setRedirectUris(Sets.newHashSet(claimSet.getStringListClaim(claim))); + break; + case CLIENT_SECRET: + throw new ValidationException("invalid_client_metadata", "Software statement can't contain client secret", HttpStatus.BAD_REQUEST); + case CLIENT_ID: + throw new ValidationException("invalid_client_metadata", "Software statement can't contain client ID", HttpStatus.BAD_REQUEST); + + default: + logger.warn("Software statement contained unknown field: " + claim + " with value " + claimSet.getClaim(claim)); + break; + } + } + + return newClient; + } catch (ParseException e) { + throw new ValidationException("invalid_client_metadata", "Software statement claims didn't parse", HttpStatus.BAD_REQUEST); + } + } else { + throw new ValidationException("invalid_client_metadata", "Software statement rejected by validator", HttpStatus.BAD_REQUEST); + } + } else { + // nothing to see here, carry on + return newClient; + } + + } + }