added software statement to client model, added processor to dynamic registration parser

pull/1108/head
Justin Richer 2016-07-21 16:55:46 -04:00
parent 17be89fe98
commit fa63993896
7 changed files with 280 additions and 5 deletions

View File

@ -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);
}

View File

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

View File

@ -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<String> 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;
}
}

View File

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

View File

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

View File

@ -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)
);

View File

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