added software statements to client API

pull/1108/head
Justin Richer 2016-07-24 16:12:56 -04:00
parent d89257380f
commit 57208ac35d
6 changed files with 258 additions and 12 deletions

View File

@ -32,19 +32,20 @@
<!-- validate incoming tokens for JWT assertions -->
<bean id="jwtAssertionValidator" class="org.mitre.jwt.assertion.impl.NullAssertionValidator" />
<!-- translate incoming assertions to token authorization objects -->
<bean id="jwtAssertionTokenFactory" class="org.mitre.oauth2.assertion.impl.DirectCopyRequestFactory" />
<!-- validate client software statements for dynamic registration -->
<bean id="clientAssertionValidator" class="org.mitre.jwt.assertion.impl.NullAssertionValidator" />
<!-- <bean id="clientAssertionValidator" class="org.mitre.jwt.assertion.impl.NullAssertionValidator" /> -->
<!-- this class will pass assertions signed by the issuers and keys in the whitelist -->
<!-- <bean id="clientAssertionValidator" class="org.mitre.jwt.assertion.impl.WhitelistedIssuerAssertionValidator"> -->
<!-- <property name="whitelist"> -->
<!-- <map> -->
<!-- <entry key="http://artemesia.local" value="http://localhost:8080/openid-connect-server-webapp/jwk" /> -->
<!-- </map> -->
<!-- </property> -->
<!-- </bean> -->
<bean id="clientAssertionValidator" class="org.mitre.jwt.assertion.impl.WhitelistedIssuerAssertionValidator">
<property name="whitelist">
<map>
<entry key="http://artemesia.local" value="http://localhost:8080/openid-connect-server-webapp/jwk" />
</map>
</property>
</bean>
</beans>

View File

@ -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,

View File

@ -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",

View File

@ -298,6 +298,16 @@
<div class="controls">
</div>
</div>
<div class="control-group" id="softwareStatement">
<label class="control-label" data-i18n="client.client-form.software-statement">Software Statement</label>
<div class="controls">
<textarea class="input-xlarge" placeholder="ejy0..." maxlength="4096"
rows="3" data-i18n="[placeholder]client.client-form.software-statement-placeholder"><%-client.softwareStatement%></textarea>
<p class="help-block" data-i18n="client.client-form.software-statement-help">A software statement is issued by a trusted third party and locks certain elements of a client's registration</p>
</div>
</div>
</div>
<div class="tab-pane" id="client-access-tab">

View File

@ -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<JWT>() {
@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();

View File

@ -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 <mjett@mitre.org>
@ -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<JWT>() {
@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;
}
}
}