diff --git a/openid-connect-common/src/main/java/org/mitre/jwt/assertion/AssertionValidator.java b/openid-connect-common/src/main/java/org/mitre/jwt/assertion/AssertionValidator.java new file mode 100644 index 000000000..c4516a973 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/jwt/assertion/AssertionValidator.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright 2016 The MITRE Corporation + * and the MIT Internet Trust Consortium + * + * 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.jwt.assertion; + +import com.nimbusds.jwt.JWT; + +/** + * @author jricher + * + */ +public interface AssertionValidator { + + public boolean isValid(JWT assertion); + +} diff --git a/openid-connect-common/src/main/java/org/mitre/jwt/assertion/impl/NullAssertionValidator.java b/openid-connect-common/src/main/java/org/mitre/jwt/assertion/impl/NullAssertionValidator.java new file mode 100644 index 000000000..60f6c06e2 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/jwt/assertion/impl/NullAssertionValidator.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright 2016 The MITRE Corporation + * and the MIT Internet Trust Consortium + * + * 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.jwt.assertion.impl; + +import org.mitre.jwt.assertion.AssertionValidator; + +import com.nimbusds.jwt.JWT; + +/** + * @author jricher + * + */ +public class NullAssertionValidator implements AssertionValidator { + + @Override + public boolean isValid(JWT assertion) { + // TODO Auto-generated method stub + return false; + + } + +} diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/model/ClientDetailsEntity.java b/openid-connect-common/src/main/java/org/mitre/oauth2/model/ClientDetailsEntity.java index 4dadbf175..8de9606ce 100644 --- a/openid-connect-common/src/main/java/org/mitre/oauth2/model/ClientDetailsEntity.java +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/model/ClientDetailsEntity.java @@ -51,6 +51,7 @@ import org.mitre.oauth2.model.convert.JWEAlgorithmStringConverter; import org.mitre.oauth2.model.convert.JWEEncryptionMethodStringConverter; import org.mitre.oauth2.model.convert.JWKSetStringConverter; import org.mitre.oauth2.model.convert.JWSAlgorithmStringConverter; +import org.mitre.oauth2.model.convert.JWTStringConverter; import org.mitre.oauth2.model.convert.SimpleGrantedAuthorityStringConverter; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.provider.ClientDetails; @@ -59,6 +60,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; /** * @author jricher @@ -144,6 +146,9 @@ public class ClientDetailsEntity implements ClientDetails { /** fields for UMA */ private Set claimsRedirectUris; + + /** Software statement **/ + private JWT softwareStatement; public enum AuthMethod { SECRET_POST("client_secret_post"), @@ -988,4 +993,21 @@ public class ClientDetailsEntity implements ClientDetails { this.claimsRedirectUris = claimsRedirectUris; } + /** + * @return the softwareStatement + */ + @Basic + @Column(name = "software_statement") + @Convert(converter = JWTStringConverter.class) + public JWT getSoftwareStatement() { + return softwareStatement; + } + + /** + * @param softwareStatement the softwareStatement to set + */ + public void setSoftwareStatement(JWT softwareStatement) { + this.softwareStatement = softwareStatement; + } + } diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/model/RegisteredClientFields.java b/openid-connect-common/src/main/java/org/mitre/oauth2/model/RegisteredClientFields.java index db236df4e..090acee3d 100644 --- a/openid-connect-common/src/main/java/org/mitre/oauth2/model/RegisteredClientFields.java +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/model/RegisteredClientFields.java @@ -17,6 +17,7 @@ package org.mitre.oauth2.model; public interface RegisteredClientFields { + public String SOFTWARE_STATEMENT = "software_statement"; public String CLAIMS_REDIRECT_URIS = "claims_redirect_uris"; public String CLIENT_SECRET_EXPIRES_AT = "client_secret_expires_at"; public String CLIENT_ID_ISSUED_AT = "client_id_issued_at"; diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/ClientDetailsEntityJsonProcessor.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/ClientDetailsEntityJsonProcessor.java index 6cabe24ae..6282c31ff 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/ClientDetailsEntityJsonProcessor.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/ClientDetailsEntityJsonProcessor.java @@ -21,6 +21,7 @@ package org.mitre.openid.connect; 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; @@ -47,9 +48,10 @@ import static org.mitre.oauth2.model.RegisteredClientFields.REQUEST_OBJECT_SIGNI 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.*; +import static org.mitre.oauth2.model.RegisteredClientFields.SCOPE; import static org.mitre.oauth2.model.RegisteredClientFields.SCOPE_SEPARATOR; 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; @@ -67,6 +69,7 @@ import static org.mitre.util.JsonUtils.getAsStringSet; import java.text.ParseException; +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; @@ -74,6 +77,8 @@ import org.mitre.oauth2.model.ClientDetailsEntity.SubjectType; import org.mitre.oauth2.model.RegisteredClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import com.google.common.base.Joiner; import com.google.common.base.Splitter; @@ -82,6 +87,8 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTParser; /** * Utility class to handle the parsing and serialization of ClientDetails objects. @@ -94,7 +101,7 @@ public class ClientDetailsEntityJsonProcessor { private static Logger logger = LoggerFactory.getLogger(ClientDetailsEntityJsonProcessor.class); private static JsonParser parser = new JsonParser(); - + /** * * Create an unbound ClientDetailsEntity from the given JSON string. @@ -148,6 +155,7 @@ public class ClientDetailsEntityJsonProcessor { c.setJwks(jwks); } catch (ParseException e) { logger.error("Unable to parse JWK Set for client", e); + return null; } } @@ -195,6 +203,16 @@ public class ClientDetailsEntityJsonProcessor { c.setClaimsRedirectUris(getAsStringSet(o, CLAIMS_REDIRECT_URIS)); + String softwareStatement = getAsString(o, SOFTWARE_STATEMENT); + try { + JWT softwareStatementJwt = JWTParser.parse(softwareStatement); + c.setSoftwareStatement(softwareStatementJwt); + } catch (ParseException e) { + logger.warn("Error parsing software statement", e); + return null; + } + + return c; } else { return null; diff --git a/openid-connect-server-webapp/src/main/resources/db/tables/hsql_database_tables.sql b/openid-connect-server-webapp/src/main/resources/db/tables/hsql_database_tables.sql index 69e74508c..e81927dff 100644 --- a/openid-connect-server-webapp/src/main/resources/db/tables/hsql_database_tables.sql +++ b/openid-connect-server-webapp/src/main/resources/db/tables/hsql_database_tables.sql @@ -169,6 +169,8 @@ CREATE TABLE IF NOT EXISTS client_details ( initiate_login_uri VARCHAR(2048), clear_access_tokens_on_refresh BOOLEAN DEFAULT true NOT NULL, + software_statement VARCHAR(4096), + UNIQUE (client_id) ); diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/DynamicClientRegistrationEndpoint.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/DynamicClientRegistrationEndpoint.java index 1aeb23328..bf3a9cfe9 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/DynamicClientRegistrationEndpoint.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/DynamicClientRegistrationEndpoint.java @@ -16,6 +16,8 @@ *******************************************************************************/ package org.mitre.openid.connect.web; +import static org.mitre.oauth2.model.RegisteredClientFields.*; + import java.io.UnsupportedEncodingException; import java.text.ParseException; import java.util.Date; @@ -23,9 +25,12 @@ import java.util.HashSet; import java.util.Set; import java.util.concurrent.TimeUnit; +import org.mitre.jwt.assertion.AssertionValidator; import org.mitre.jwt.signer.service.JWTSigningAndValidationService; 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.OAuth2AccessTokenEntity; import org.mitre.oauth2.model.RegisteredClient; import org.mitre.oauth2.model.SystemScope; @@ -43,9 +48,11 @@ 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.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.oauth2.common.util.OAuth2Utils; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails; import org.springframework.stereotype.Controller; @@ -60,6 +67,11 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import com.google.gson.JsonSyntaxException; +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.JWTClaimsSet; @Controller @RequestMapping(value = DynamicClientRegistrationEndpoint.URL) @@ -88,6 +100,10 @@ public class DynamicClientRegistrationEndpoint { @Autowired private OIDCTokenService connectTokenService; + @Autowired + @Qualifier("clientAssertionValidator") + private static AssertionValidator assertionValidator; + /** * Logger for this class */ @@ -132,6 +148,7 @@ public class DynamicClientRegistrationEndpoint { newClient = validateGrantTypes(newClient); newClient = validateRedirectUris(newClient); newClient = validateAuth(newClient); + newClient = validateSoftwareStatement(newClient); } catch (ValidationException ve) { // validation failed, return an error m.addAttribute(JsonErrorView.ERROR, ve.getError()); @@ -234,7 +251,7 @@ public class DynamicClientRegistrationEndpoint { if (client != null && client.getClientId().equals(auth.getOAuth2Request().getClientId())) { try { - OAuth2AccessTokenEntity token = fetchValidRegistrationToken(auth, client); + OAuth2AccessTokenEntity token = rotateRegistrationTokenIfNecessary(auth, client); RegisteredClient registered = new RegisteredClient(client, token.getValue(), config.getIssuer() + "register/" + UriUtils.encodePathSegment(client.getClientId(), "UTF-8")); // send it all out to the view @@ -321,7 +338,7 @@ public class DynamicClientRegistrationEndpoint { // save the client ClientDetailsEntity savedClient = clientService.updateClient(oldClient, newClient); - OAuth2AccessTokenEntity token = fetchValidRegistrationToken(auth, savedClient); + OAuth2AccessTokenEntity token = rotateRegistrationTokenIfNecessary(auth, savedClient); RegisteredClient registered = new RegisteredClient(savedClient, token.getValue(), config.getIssuer() + "register/" + UriUtils.encodePathSegment(savedClient.getClientId(), "UTF-8")); @@ -553,7 +570,155 @@ public class DynamicClientRegistrationEndpoint { return newClient; } - private OAuth2AccessTokenEntity fetchValidRegistrationToken(OAuth2Authentication auth, ClientDetailsEntity client) { + + /** + * @param newClient + * @return + * @throws ValidationException + */ + 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; + } + + } + + + /* + * Rotates the registration token if it's expired, otherwise returns it + */ + private OAuth2AccessTokenEntity rotateRegistrationTokenIfNecessary(OAuth2Authentication auth, ClientDetailsEntity client) { OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) auth.getDetails(); OAuth2AccessTokenEntity token = tokenService.readAccessToken(details.getTokenValue());