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;
+ }
+
+ }
+
}