ClientAPI.java

/*******************************************************************************
 * Copyright 2017 The MIT Internet Trust Consortium
 *
 * Portions copyright 2011-2013 The MITRE Corporation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *******************************************************************************/
package org.mitre.openid.connect.web;

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.model.PKCEAlgorithm;
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;
import org.mitre.openid.connect.view.ClientEntityViewForUsers;
import org.mitre.openid.connect.view.HttpCodeView;
import org.mitre.openid.connect.view.JsonEntityView;
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;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
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;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
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>
 */

@Controller
@RequestMapping("/" + ClientAPI.URL)
@PreAuthorize("hasRole('ROLE_USER')")
public class ClientAPI {

	public static final String URL = RootController.API_URL + "/clients";

	@Autowired
	private ClientDetailsEntityService clientService;

	@Autowired
	private ClientLogoLoadingService clientLogoLoadingService;

	@Autowired
	@Qualifier("clientAssertionValidator")
	private AssertionValidator assertionValidator;

	private JsonParser parser = new JsonParser();

	private Gson gson = new GsonBuilder()
			.serializeNulls()
			.registerTypeAdapter(JWSAlgorithm.class, new JsonDeserializer<Algorithm>() {
				@Override
				public JWSAlgorithm deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
					if (json.isJsonPrimitive()) {
						return JWSAlgorithm.parse(json.getAsString());
					} else {
						return null;
					}
				}
			})
			.registerTypeAdapter(JWEAlgorithm.class, new JsonDeserializer<Algorithm>() {
				@Override
				public JWEAlgorithm deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
					if (json.isJsonPrimitive()) {
						return JWEAlgorithm.parse(json.getAsString());
					} else {
						return null;
					}
				}
			})
			.registerTypeAdapter(EncryptionMethod.class, new JsonDeserializer<Algorithm>() {
				@Override
				public EncryptionMethod deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
					if (json.isJsonPrimitive()) {
						return EncryptionMethod.parse(json.getAsString());
					} else {
						return null;
					}
				}
			})
			.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;
					}
				}
			})
			.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;
					}
				}
			})
			.registerTypeAdapter(PKCEAlgorithm.class, new JsonDeserializer<Algorithm>() {
				@Override
				public PKCEAlgorithm deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
					if (json.isJsonPrimitive()) {
						return PKCEAlgorithm.parse(json.getAsString());
					} else {
						return null;
					}
				}
			})
			.setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
			.create();

	/**
	 * Logger for this class
	 */
	private static final Logger logger = LoggerFactory.getLogger(ClientAPI.class);

	/**
	 * Get a list of all clients
	 * @param modelAndView
	 * @return
	 */
	@RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
	public String apiGetAllClients(Model model, Authentication auth) {

		Collection<ClientDetailsEntity> clients = clientService.getAllClients();
		model.addAttribute(JsonEntityView.ENTITY, clients);

		if (AuthenticationUtilities.isAdmin(auth)) {
			return ClientEntityViewForAdmins.VIEWNAME;
		} else {
			return ClientEntityViewForUsers.VIEWNAME;
		}
	}

	/**
	 * Create a new client
	 * @param json
	 * @param m
	 * @param principal
	 * @return
	 */
	@PreAuthorize("hasRole('ROLE_ADMIN')")
	@RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
	public String apiAddClient(@RequestBody String jsonString, Model m, Authentication auth) {

		JsonObject json = null;
		ClientDetailsEntity client = null;

		try {
			json = parser.parse(jsonString).getAsJsonObject();
			client = gson.fromJson(json, ClientDetailsEntity.class);
			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.");
			return JsonErrorView.VIEWNAME;
		} catch (IllegalStateException e) {
			logger.error("apiAddClient failed due to IllegalStateException", e);
			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
		if (Strings.isNullOrEmpty(client.getClientId())) {
			client = clientService.generateClientId(client);
		}

		if (client.getTokenEndpointAuthMethod() == null ||
				client.getTokenEndpointAuthMethod().equals(AuthMethod.NONE)) {
			// we shouldn't have a secret for this client

			client.setClientSecret(null);

		} else if (client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_BASIC)
				|| client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_POST)
				|| client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_JWT)) {

			// if they've asked for us to generate a client secret (or they left it blank but require one), do so here
			if (json.has("generateClientSecret") && json.get("generateClientSecret").getAsBoolean()
					|| Strings.isNullOrEmpty(client.getClientSecret())) {
				client = clientService.generateClientSecret(client);
			}

		} else if (client.getTokenEndpointAuthMethod().equals(AuthMethod.PRIVATE_KEY)) {

			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 JWK Set URI or JWK Set Value.");
				return JsonErrorView.VIEWNAME;
			}

			// otherwise we shouldn't have a secret for this client
			client.setClientSecret(null);

		} else {

			logger.error("unknown auth method");
			m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST);
			m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unknown auth method requested");
			return JsonErrorView.VIEWNAME;


		}

		client.setDynamicallyRegistered(false);

		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: " + e.getMessage());
			return JsonErrorView.VIEWNAME;
		} catch (PersistenceException e) {
			Throwable cause = e.getCause();
			if (cause instanceof DatabaseException) {
				Throwable databaseExceptionCause = cause.getCause();
				if(databaseExceptionCause instanceof SQLIntegrityConstraintViolationException) {
					logger.error("apiAddClient failed; duplicate client id entry found: {}", client.getClientId());
					m.addAttribute(HttpCodeView.CODE, HttpStatus.CONFLICT);
					m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unable to save client. Duplicate client id entry found: " + client.getClientId());
					return JsonErrorView.VIEWNAME;
				}
			}
			throw e;
		}
	}

	/**
	 * Update an existing client
	 * @param id
	 * @param jsonString
	 * @param m
	 * @param principal
	 * @return
	 */
	@PreAuthorize("hasRole('ROLE_ADMIN')")
	@RequestMapping(value="/{id}", method = RequestMethod.PUT, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
	public String apiUpdateClient(@PathVariable("id") Long id, @RequestBody String jsonString, Model m, Authentication auth) {

		JsonObject json = null;
		ClientDetailsEntity client = null;

		try {
			// 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);
			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.");
			return JsonErrorView.VIEWNAME;
		} catch (IllegalStateException e) {
			logger.error("apiUpdateClient failed due to IllegalStateException", e);
			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);

		if (oldClient == null) {
			logger.error("apiUpdateClient failed; client with id " + id + " could not be found.");
			m.addAttribute(HttpCodeView.CODE, HttpStatus.NOT_FOUND);
			m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Could not update client. The requested client with id " + id + "could not be found.");
			return JsonErrorView.VIEWNAME;
		}

		// if they leave the client identifier empty, force it to be generated
		if (Strings.isNullOrEmpty(client.getClientId())) {
			client = clientService.generateClientId(client);
		}

		if (client.getTokenEndpointAuthMethod() == null ||
				client.getTokenEndpointAuthMethod().equals(AuthMethod.NONE)) {
			// we shouldn't have a secret for this client

			client.setClientSecret(null);

		} else if (client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_BASIC)
				|| client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_POST)
				|| client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_JWT)) {

			// if they've asked for us to generate a client secret (or they left it blank but require one), do so here
			if (json.has("generateClientSecret") && json.get("generateClientSecret").getAsBoolean()
					|| Strings.isNullOrEmpty(client.getClientSecret())) {
				client = clientService.generateClientSecret(client);
			}

		} else if (client.getTokenEndpointAuthMethod().equals(AuthMethod.PRIVATE_KEY)) {

			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 JWK Set URI or JWK Set Value.");
				return JsonErrorView.VIEWNAME;
			}

			// otherwise we shouldn't have a secret for this client
			client.setClientSecret(null);

		} else {

			logger.error("unknown auth method");
			m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST);
			m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unknown auth method requested");
			return JsonErrorView.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: " + e.getMessage());
			return JsonErrorView.VIEWNAME;
		}
	}

	/**
	 * Delete a client
	 * @param id
	 * @param modelAndView
	 * @return
	 */
	@PreAuthorize("hasRole('ROLE_ADMIN')")
	@RequestMapping(value="/{id}", method=RequestMethod.DELETE)
	public String apiDeleteClient(@PathVariable("id") Long id, ModelAndView modelAndView) {

		ClientDetailsEntity client = clientService.getClientById(id);

		if (client == null) {
			logger.error("apiDeleteClient failed; client with id " + id + " could not be found.");
			modelAndView.getModelMap().put(HttpCodeView.CODE, HttpStatus.NOT_FOUND);
			modelAndView.getModelMap().put(JsonErrorView.ERROR_MESSAGE, "Could not delete client. The requested client with id " + id + "could not be found.");
			return JsonErrorView.VIEWNAME;
		} else {
			modelAndView.getModelMap().put(HttpCodeView.CODE, HttpStatus.OK);
			clientService.deleteClient(client);
		}

		return HttpCodeView.VIEWNAME;
	}


	/**
	 * Get an individual client
	 * @param id
	 * @param modelAndView
	 * @return
	 */
	@RequestMapping(value="/{id}", method=RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
	public String apiShowClient(@PathVariable("id") Long id, Model model, Authentication auth) {

		ClientDetailsEntity client = clientService.getClientById(id);

		if (client == null) {
			logger.error("apiShowClient failed; client with id " + id + " could not be found.");
			model.addAttribute(HttpCodeView.CODE, HttpStatus.NOT_FOUND);
			model.addAttribute(JsonErrorView.ERROR_MESSAGE, "The requested client with id " + id + " could not be found.");
			return JsonErrorView.VIEWNAME;
		}

		model.addAttribute(JsonEntityView.ENTITY, client);

		if (AuthenticationUtilities.isAdmin(auth)) {
			return ClientEntityViewForAdmins.VIEWNAME;
		} else {
			return ClientEntityViewForUsers.VIEWNAME;
		}
	}

	/**
	 * Get the logo image for a client
	 * @param id
	 */
	@RequestMapping(value = "/{id}/logo", method=RequestMethod.GET, produces = { MediaType.IMAGE_GIF_VALUE, MediaType.IMAGE_JPEG_VALUE, MediaType.IMAGE_PNG_VALUE })
	public ResponseEntity<byte[]> getClientLogo(@PathVariable("id") Long id, Model model) {

		ClientDetailsEntity client = clientService.getClientById(id);

		if (client == null) {
			return new ResponseEntity<>(HttpStatus.NOT_FOUND);
		} else if (Strings.isNullOrEmpty(client.getLogoUri())) {
			return new ResponseEntity<>(HttpStatus.NOT_FOUND);
		} else {
			// get the image from cache
			CachedImage image = clientLogoLoadingService.getLogo(client);

			HttpHeaders headers = new HttpHeaders();
			headers.setContentType(MediaType.parseMediaType(image.getContentType()));
			headers.setContentLength(image.getLength());

			return new ResponseEntity<>(image.getData(), headers, HttpStatus.OK);
		}
	}

	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.getJSONObjectClaim(claim).toJSONString()));
								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;
		}

	}

}