From 12bfab4f557d635247fda34d89337fea45f7e418 Mon Sep 17 00:00:00 2001 From: Andrea Ceccanti Date: Mon, 25 Oct 2021 17:13:07 +0200 Subject: [PATCH] MitreID code compiles against latest Spring libraries --- .../oauth2/model/OAuth2AccessTokenEntity.java | 4 - .../UriEncodedClientUserDetailsService.java | 3 +- .../DynamicClientRegistrationEndpoint.java | 32 +- ...ProtectedResourceRegistrationEndpoint.java | 834 +++++++++--------- pom.xml | 11 +- 5 files changed, 441 insertions(+), 443 deletions(-) diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/model/OAuth2AccessTokenEntity.java b/openid-connect-common/src/main/java/org/mitre/oauth2/model/OAuth2AccessTokenEntity.java index d1bda807b..40472f1d9 100644 --- a/openid-connect-common/src/main/java/org/mitre/oauth2/model/OAuth2AccessTokenEntity.java +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/model/OAuth2AccessTokenEntity.java @@ -50,8 +50,6 @@ import org.mitre.oauth2.model.convert.JWTStringConverter; import org.mitre.openid.connect.model.ApprovedSite; import org.mitre.uma.model.Permission; import org.springframework.security.oauth2.common.OAuth2AccessToken; -import org.springframework.security.oauth2.common.OAuth2AccessTokenJackson1Deserializer; -import org.springframework.security.oauth2.common.OAuth2AccessTokenJackson1Serializer; import org.springframework.security.oauth2.common.OAuth2AccessTokenJackson2Deserializer; import org.springframework.security.oauth2.common.OAuth2AccessTokenJackson2Serializer; import org.springframework.security.oauth2.common.OAuth2RefreshToken; @@ -74,8 +72,6 @@ import com.nimbusds.jwt.JWT; @NamedQuery(name = OAuth2AccessTokenEntity.QUERY_BY_RESOURCE_SET, query = "select a from OAuth2AccessTokenEntity a join a.permissions p where p.resourceSet.id = :" + OAuth2AccessTokenEntity.PARAM_RESOURCE_SET_ID), @NamedQuery(name = OAuth2AccessTokenEntity.QUERY_BY_NAME, query = "select r from OAuth2AccessTokenEntity r where r.authenticationHolder.userAuth.name = :" + OAuth2AccessTokenEntity.PARAM_NAME) }) -@org.codehaus.jackson.map.annotate.JsonSerialize(using = OAuth2AccessTokenJackson1Serializer.class) -@org.codehaus.jackson.map.annotate.JsonDeserialize(using = OAuth2AccessTokenJackson1Deserializer.class) @com.fasterxml.jackson.databind.annotation.JsonSerialize(using = OAuth2AccessTokenJackson2Serializer.class) @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = OAuth2AccessTokenJackson2Deserializer.class) public class OAuth2AccessTokenEntity implements OAuth2AccessToken { diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/service/impl/UriEncodedClientUserDetailsService.java b/openid-connect-common/src/main/java/org/mitre/oauth2/service/impl/UriEncodedClientUserDetailsService.java index 335efbf18..e032e28e0 100644 --- a/openid-connect-common/src/main/java/org/mitre/oauth2/service/impl/UriEncodedClientUserDetailsService.java +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/service/impl/UriEncodedClientUserDetailsService.java @@ -15,7 +15,6 @@ *******************************************************************************/ package org.mitre.oauth2.service.impl; -import java.io.UnsupportedEncodingException; import java.math.BigInteger; import java.security.SecureRandom; import java.util.Collection; @@ -91,7 +90,7 @@ public class UriEncodedClientUserDetailsService implements UserDetailsService { } else { throw new UsernameNotFoundException("Client not found: " + clientId); } - } catch (UnsupportedEncodingException | InvalidClientException e) { + } catch (InvalidClientException e) { throw new UsernameNotFoundException("Client not found: " + clientId); } 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 c3373c5ed..2eb403314 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 @@ -17,7 +17,6 @@ *******************************************************************************/ package org.mitre.openid.connect.web; -import java.io.UnsupportedEncodingException; import java.text.ParseException; import java.util.Date; import java.util.Set; @@ -218,10 +217,6 @@ public class DynamicClientRegistrationEndpoint { m.addAttribute(HttpCodeView.CODE, HttpStatus.CREATED); // http 201 return ClientInformationResponseView.VIEWNAME; - } catch (UnsupportedEncodingException e) { - logger.error("Unsupported encoding", e); - m.addAttribute(HttpCodeView.CODE, HttpStatus.INTERNAL_SERVER_ERROR); - return HttpCodeView.VIEWNAME; } catch (IllegalArgumentException e) { logger.error("Couldn't save client", e); @@ -260,23 +255,16 @@ public class DynamicClientRegistrationEndpoint { ClientDetailsEntity client = clientService.loadClientByClientId(clientId); if (client != null && client.getClientId().equals(auth.getOAuth2Request().getClientId())) { + OAuth2AccessTokenEntity token = rotateRegistrationTokenIfNecessary(auth, client); + RegisteredClient registered = + new RegisteredClient(client, token.getValue(), config.getIssuer() + "register/" + + UriUtils.encodePathSegment(client.getClientId(), "UTF-8")); - try { - 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 + m.addAttribute("client", registered); + m.addAttribute(HttpCodeView.CODE, HttpStatus.OK); // http 200 - // send it all out to the view - m.addAttribute("client", registered); - m.addAttribute(HttpCodeView.CODE, HttpStatus.OK); // http 200 - - return ClientInformationResponseView.VIEWNAME; - } catch (UnsupportedEncodingException e) { - logger.error("Unsupported encoding", e); - m.addAttribute(HttpCodeView.CODE, HttpStatus.INTERNAL_SERVER_ERROR); - return HttpCodeView.VIEWNAME; - } + return ClientInformationResponseView.VIEWNAME; } else { // client mismatch @@ -380,10 +368,6 @@ public class DynamicClientRegistrationEndpoint { m.addAttribute(HttpCodeView.CODE, HttpStatus.OK); // http 200 return ClientInformationResponseView.VIEWNAME; - } catch (UnsupportedEncodingException e) { - logger.error("Unsupported encoding", e); - m.addAttribute(HttpCodeView.CODE, HttpStatus.INTERNAL_SERVER_ERROR); - return HttpCodeView.VIEWNAME; } catch (IllegalArgumentException e) { logger.error("Couldn't save client", e); diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ProtectedResourceRegistrationEndpoint.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ProtectedResourceRegistrationEndpoint.java index 9e2e89b33..9c15982bd 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ProtectedResourceRegistrationEndpoint.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ProtectedResourceRegistrationEndpoint.java @@ -15,7 +15,6 @@ *******************************************************************************/ package org.mitre.openid.connect.web; -import java.io.UnsupportedEncodingException; import java.text.ParseException; import java.util.Date; import java.util.HashSet; @@ -59,412 +58,431 @@ import com.google.gson.JsonSyntaxException; @RequestMapping(value = ProtectedResourceRegistrationEndpoint.URL) public class ProtectedResourceRegistrationEndpoint { - /** - * - */ - public static final String URL = "resource"; - - @Autowired - private ClientDetailsEntityService clientService; - - @Autowired - private OAuth2TokenEntityService tokenService; - - @Autowired - private SystemScopeService scopeService; - - @Autowired - private ConfigurationPropertiesBean config; - - @Autowired - private OIDCTokenService connectTokenService; - - /** - * Logger for this class - */ - private static final Logger logger = LoggerFactory.getLogger(ProtectedResourceRegistrationEndpoint.class); - - /** - * Create a new Client, issue a client ID, and create a registration access token. - * @param jsonString - * @param m - * @param p - * @return - */ - @RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public String registerNewProtectedResource(@RequestBody String jsonString, Model m) { - - ClientDetailsEntity newClient = null; - try { - newClient = ClientDetailsEntityJsonProcessor.parse(jsonString); - } catch (JsonSyntaxException e) { - // bad parse - // didn't parse, this is a bad request - logger.error("registerNewProtectedResource failed; submitted JSON is malformed"); - m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400 - return HttpCodeView.VIEWNAME; - } - - if (newClient != null) { - // it parsed! - - // - // Now do some post-processing consistency checks on it - // - - // clear out any spurious id/secret (clients don't get to pick) - newClient.setClientId(null); - newClient.setClientSecret(null); - - // do validation on the fields - try { - newClient = validateScopes(newClient); - newClient = validateAuth(newClient); - } catch (ValidationException ve) { - // validation failed, return an error - m.addAttribute(JsonErrorView.ERROR, ve.getError()); - m.addAttribute(JsonErrorView.ERROR_MESSAGE, ve.getErrorDescription()); - m.addAttribute(HttpCodeView.CODE, ve.getStatus()); - return JsonErrorView.VIEWNAME; - } - - - // no grant types are allowed - newClient.setGrantTypes(new HashSet()); - newClient.setResponseTypes(new HashSet()); - newClient.setRedirectUris(new HashSet()); - - // don't issue tokens to this client - newClient.setAccessTokenValiditySeconds(0); - newClient.setIdTokenValiditySeconds(0); - newClient.setRefreshTokenValiditySeconds(0); - - // clear out unused fields - newClient.setDefaultACRvalues(new HashSet()); - newClient.setDefaultMaxAge(null); - newClient.setIdTokenEncryptedResponseAlg(null); - newClient.setIdTokenEncryptedResponseEnc(null); - newClient.setIdTokenSignedResponseAlg(null); - newClient.setInitiateLoginUri(null); - newClient.setPostLogoutRedirectUris(null); - newClient.setRequestObjectSigningAlg(null); - newClient.setRequireAuthTime(null); - newClient.setReuseRefreshToken(false); - newClient.setSectorIdentifierUri(null); - newClient.setSubjectType(null); - newClient.setUserInfoEncryptedResponseAlg(null); - newClient.setUserInfoEncryptedResponseEnc(null); - newClient.setUserInfoSignedResponseAlg(null); - - // this client has been dynamically registered (obviously) - newClient.setDynamicallyRegistered(true); - - // this client has access to the introspection endpoint - newClient.setAllowIntrospection(true); - - // now save it - try { - ClientDetailsEntity savedClient = clientService.saveNewClient(newClient); - - // generate the registration access token - OAuth2AccessTokenEntity token = connectTokenService.createResourceAccessToken(savedClient); - tokenService.saveAccessToken(token); - - // send it all out to the view - - RegisteredClient registered = new RegisteredClient(savedClient, token.getValue(), config.getIssuer() + "resource/" + UriUtils.encodePathSegment(savedClient.getClientId(), "UTF-8")); - m.addAttribute("client", registered); - m.addAttribute(HttpCodeView.CODE, HttpStatus.CREATED); // http 201 - - return ClientInformationResponseView.VIEWNAME; - } catch (UnsupportedEncodingException e) { - logger.error("Unsupported encoding", e); - m.addAttribute(HttpCodeView.CODE, HttpStatus.INTERNAL_SERVER_ERROR); - return HttpCodeView.VIEWNAME; - } catch (IllegalArgumentException e) { - logger.error("Couldn't save client", e); - - m.addAttribute(JsonErrorView.ERROR, "invalid_client_metadata"); - m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unable to save client due to invalid or inconsistent metadata."); - m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400 - - return JsonErrorView.VIEWNAME; - } - } else { - // didn't parse, this is a bad request - logger.error("registerNewClient failed; submitted JSON is malformed"); - m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400 - - return HttpCodeView.VIEWNAME; - } - - } - - private ClientDetailsEntity validateScopes(ClientDetailsEntity newClient) throws ValidationException { - // scopes that the client is asking for - Set requestedScopes = scopeService.fromStrings(newClient.getScope()); - - // the scopes that the client can have must be a subset of the dynamically allowed scopes - Set allowedScopes = scopeService.removeRestrictedAndReservedScopes(requestedScopes); - - // if the client didn't ask for any, give them the defaults - if (allowedScopes == null || allowedScopes.isEmpty()) { - allowedScopes = scopeService.getDefaults(); - } - - newClient.setScope(scopeService.toStrings(allowedScopes)); - - return newClient; - } - - /** - * Get the meta information for a client. - * @param clientId - * @param m - * @param auth - * @return - */ - @PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + SystemScopeService.RESOURCE_TOKEN_SCOPE + "')") - @RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) - public String readResourceConfiguration(@PathVariable("id") String clientId, Model m, OAuth2Authentication auth) { - - ClientDetailsEntity client = clientService.loadClientByClientId(clientId); - - if (client != null && client.getClientId().equals(auth.getOAuth2Request().getClientId())) { - - - - try { - // possibly update the token - OAuth2AccessTokenEntity token = fetchValidRegistrationToken(auth, client); - - RegisteredClient registered = new RegisteredClient(client, token.getValue(), config.getIssuer() + "resource/" + UriUtils.encodePathSegment(client.getClientId(), "UTF-8")); - - // send it all out to the view - m.addAttribute("client", registered); - m.addAttribute(HttpCodeView.CODE, HttpStatus.OK); // http 200 - - return ClientInformationResponseView.VIEWNAME; - } catch (UnsupportedEncodingException e) { - logger.error("Unsupported encoding", e); - m.addAttribute(HttpCodeView.CODE, HttpStatus.INTERNAL_SERVER_ERROR); - return HttpCodeView.VIEWNAME; - } - } else { - // client mismatch - logger.error("readResourceConfiguration failed, client ID mismatch: " - + clientId + " and " + auth.getOAuth2Request().getClientId() + " do not match."); - m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); // http 403 - - return HttpCodeView.VIEWNAME; - } - } - - /** - * Update the metainformation for a given client. - * @param clientId - * @param jsonString - * @param m - * @param auth - * @return - */ - @PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + SystemScopeService.RESOURCE_TOKEN_SCOPE + "')") - @RequestMapping(value = "/{id}", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) - public String updateProtectedResource(@PathVariable("id") String clientId, @RequestBody String jsonString, Model m, OAuth2Authentication auth) { - - - ClientDetailsEntity newClient = null; - try { - newClient = ClientDetailsEntityJsonProcessor.parse(jsonString); - } catch (JsonSyntaxException e) { - // bad parse - // didn't parse, this is a bad request - logger.error("updateProtectedResource failed; submitted JSON is malformed"); - m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400 - return HttpCodeView.VIEWNAME; - } - - ClientDetailsEntity oldClient = clientService.loadClientByClientId(clientId); - - if (newClient != null && oldClient != null // we have an existing client and the new one parsed - && oldClient.getClientId().equals(auth.getOAuth2Request().getClientId()) // the client passed in the URI matches the one in the auth - && oldClient.getClientId().equals(newClient.getClientId()) // the client passed in the body matches the one in the URI - ) { - - // a client can't ask to update its own client secret to any particular value - newClient.setClientSecret(oldClient.getClientSecret()); - - newClient.setCreatedAt(oldClient.getCreatedAt()); - - // no grant types are allowed - newClient.setGrantTypes(new HashSet()); - newClient.setResponseTypes(new HashSet()); - newClient.setRedirectUris(new HashSet()); - - // don't issue tokens to this client - newClient.setAccessTokenValiditySeconds(0); - newClient.setIdTokenValiditySeconds(0); - newClient.setRefreshTokenValiditySeconds(0); - - // clear out unused fields - newClient.setDefaultACRvalues(new HashSet()); - newClient.setDefaultMaxAge(null); - newClient.setIdTokenEncryptedResponseAlg(null); - newClient.setIdTokenEncryptedResponseEnc(null); - newClient.setIdTokenSignedResponseAlg(null); - newClient.setInitiateLoginUri(null); - newClient.setPostLogoutRedirectUris(null); - newClient.setRequestObjectSigningAlg(null); - newClient.setRequireAuthTime(null); - newClient.setReuseRefreshToken(false); - newClient.setSectorIdentifierUri(null); - newClient.setSubjectType(null); - newClient.setUserInfoEncryptedResponseAlg(null); - newClient.setUserInfoEncryptedResponseEnc(null); - newClient.setUserInfoSignedResponseAlg(null); - - // this client has been dynamically registered (obviously) - newClient.setDynamicallyRegistered(true); - - // this client has access to the introspection endpoint - newClient.setAllowIntrospection(true); - - // do validation on the fields - try { - newClient = validateScopes(newClient); - newClient = validateAuth(newClient); - } catch (ValidationException ve) { - // validation failed, return an error - m.addAttribute(JsonErrorView.ERROR, ve.getError()); - m.addAttribute(JsonErrorView.ERROR_MESSAGE, ve.getErrorDescription()); - m.addAttribute(HttpCodeView.CODE, ve.getStatus()); - return JsonErrorView.VIEWNAME; - } - - - try { - // save the client - ClientDetailsEntity savedClient = clientService.updateClient(oldClient, newClient); - - // possibly update the token - OAuth2AccessTokenEntity token = fetchValidRegistrationToken(auth, savedClient); - - RegisteredClient registered = new RegisteredClient(savedClient, token.getValue(), config.getIssuer() + "resource/" + UriUtils.encodePathSegment(savedClient.getClientId(), "UTF-8")); - - // send it all out to the view - m.addAttribute("client", registered); - m.addAttribute(HttpCodeView.CODE, HttpStatus.OK); // http 200 - - return ClientInformationResponseView.VIEWNAME; - } catch (UnsupportedEncodingException e) { - logger.error("Unsupported encoding", e); - m.addAttribute(HttpCodeView.CODE, HttpStatus.INTERNAL_SERVER_ERROR); - return HttpCodeView.VIEWNAME; - } catch (IllegalArgumentException e) { - logger.error("Couldn't save client", e); - - m.addAttribute(JsonErrorView.ERROR, "invalid_client_metadata"); - m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unable to save client due to invalid or inconsistent metadata."); - m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400 - - return JsonErrorView.VIEWNAME; - } - } else { - // client mismatch - logger.error("updateProtectedResource" + - " failed, client ID mismatch: " - + clientId + " and " + auth.getOAuth2Request().getClientId() + " do not match."); - m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); // http 403 - - return HttpCodeView.VIEWNAME; - } - } - - /** - * Delete the indicated client from the system. - * @param clientId - * @param m - * @param auth - * @return - */ - @PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + SystemScopeService.RESOURCE_TOKEN_SCOPE + "')") - @RequestMapping(value = "/{id}", method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_VALUE) - public String deleteResource(@PathVariable("id") String clientId, Model m, OAuth2Authentication auth) { - - ClientDetailsEntity client = clientService.loadClientByClientId(clientId); - - if (client != null && client.getClientId().equals(auth.getOAuth2Request().getClientId())) { - - clientService.deleteClient(client); - - m.addAttribute(HttpCodeView.CODE, HttpStatus.NO_CONTENT); // http 204 - - return HttpCodeView.VIEWNAME; - } else { - // client mismatch - logger.error("readClientConfiguration failed, client ID mismatch: " - + clientId + " and " + auth.getOAuth2Request().getClientId() + " do not match."); - m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); // http 403 - - return HttpCodeView.VIEWNAME; - } - } - - private ClientDetailsEntity validateAuth(ClientDetailsEntity newClient) throws ValidationException { - if (newClient.getTokenEndpointAuthMethod() == null) { - newClient.setTokenEndpointAuthMethod(AuthMethod.SECRET_BASIC); - } - - if (newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_BASIC || - newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_JWT || - newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_POST) { - - if (Strings.isNullOrEmpty(newClient.getClientSecret())) { - // no secret yet, we need to generate a secret - newClient = clientService.generateClientSecret(newClient); - } - } else if (newClient.getTokenEndpointAuthMethod() == AuthMethod.PRIVATE_KEY) { - if (Strings.isNullOrEmpty(newClient.getJwksUri()) && newClient.getJwks() == null) { - throw new ValidationException("invalid_client_metadata", "JWK Set URI required when using private key authentication", HttpStatus.BAD_REQUEST); - } - - newClient.setClientSecret(null); - } else if (newClient.getTokenEndpointAuthMethod() == AuthMethod.NONE) { - newClient.setClientSecret(null); - } else { - throw new ValidationException("invalid_client_metadata", "Unknown authentication method", HttpStatus.BAD_REQUEST); - } - return newClient; - } - - private OAuth2AccessTokenEntity fetchValidRegistrationToken(OAuth2Authentication auth, ClientDetailsEntity client) { - - OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) auth.getDetails(); - OAuth2AccessTokenEntity token = tokenService.readAccessToken(details.getTokenValue()); - - if (config.getRegTokenLifeTime() != null) { - - try { - // Re-issue the token if it has been issued before [currentTime - validity] - Date validToDate = new Date(System.currentTimeMillis() - config.getRegTokenLifeTime() * 1000); - if(token.getJwt().getJWTClaimsSet().getIssueTime().before(validToDate)) { - logger.info("Rotating the registration access token for " + client.getClientId()); - tokenService.revokeAccessToken(token); - OAuth2AccessTokenEntity newToken = connectTokenService.createResourceAccessToken(client); - tokenService.saveAccessToken(newToken); - return newToken; - } else { - // it's not expired, keep going - return token; - } - } catch (ParseException e) { - logger.error("Couldn't parse a known-valid token?", e); - return token; - } - } else { - // tokens don't expire, just return it - return token; - } - } + /** + * + */ + public static final String URL = "resource"; + + @Autowired + private ClientDetailsEntityService clientService; + + @Autowired + private OAuth2TokenEntityService tokenService; + + @Autowired + private SystemScopeService scopeService; + + @Autowired + private ConfigurationPropertiesBean config; + + @Autowired + private OIDCTokenService connectTokenService; + + /** + * Logger for this class + */ + private static final Logger logger = + LoggerFactory.getLogger(ProtectedResourceRegistrationEndpoint.class); + + /** + * Create a new Client, issue a client ID, and create a registration access token. + * + * @param jsonString + * @param m + * @param p + * @return + */ + @RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE) + public String registerNewProtectedResource(@RequestBody String jsonString, Model m) { + + ClientDetailsEntity newClient = null; + try { + newClient = ClientDetailsEntityJsonProcessor.parse(jsonString); + } catch (JsonSyntaxException e) { + // bad parse + // didn't parse, this is a bad request + logger.error("registerNewProtectedResource failed; submitted JSON is malformed"); + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400 + return HttpCodeView.VIEWNAME; + } + + if (newClient != null) { + // it parsed! + + // + // Now do some post-processing consistency checks on it + // + + // clear out any spurious id/secret (clients don't get to pick) + newClient.setClientId(null); + newClient.setClientSecret(null); + + // do validation on the fields + try { + newClient = validateScopes(newClient); + newClient = validateAuth(newClient); + } catch (ValidationException ve) { + // validation failed, return an error + m.addAttribute(JsonErrorView.ERROR, ve.getError()); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, ve.getErrorDescription()); + m.addAttribute(HttpCodeView.CODE, ve.getStatus()); + return JsonErrorView.VIEWNAME; + } + + + // no grant types are allowed + newClient.setGrantTypes(new HashSet()); + newClient.setResponseTypes(new HashSet()); + newClient.setRedirectUris(new HashSet()); + + // don't issue tokens to this client + newClient.setAccessTokenValiditySeconds(0); + newClient.setIdTokenValiditySeconds(0); + newClient.setRefreshTokenValiditySeconds(0); + + // clear out unused fields + newClient.setDefaultACRvalues(new HashSet()); + newClient.setDefaultMaxAge(null); + newClient.setIdTokenEncryptedResponseAlg(null); + newClient.setIdTokenEncryptedResponseEnc(null); + newClient.setIdTokenSignedResponseAlg(null); + newClient.setInitiateLoginUri(null); + newClient.setPostLogoutRedirectUris(null); + newClient.setRequestObjectSigningAlg(null); + newClient.setRequireAuthTime(null); + newClient.setReuseRefreshToken(false); + newClient.setSectorIdentifierUri(null); + newClient.setSubjectType(null); + newClient.setUserInfoEncryptedResponseAlg(null); + newClient.setUserInfoEncryptedResponseEnc(null); + newClient.setUserInfoSignedResponseAlg(null); + + // this client has been dynamically registered (obviously) + newClient.setDynamicallyRegistered(true); + + // this client has access to the introspection endpoint + newClient.setAllowIntrospection(true); + + // now save it + try { + ClientDetailsEntity savedClient = clientService.saveNewClient(newClient); + + // generate the registration access token + OAuth2AccessTokenEntity token = connectTokenService.createResourceAccessToken(savedClient); + tokenService.saveAccessToken(token); + + // send it all out to the view + + RegisteredClient registered = + new RegisteredClient(savedClient, token.getValue(), config.getIssuer() + "resource/" + + UriUtils.encodePathSegment(savedClient.getClientId(), "UTF-8")); + m.addAttribute("client", registered); + m.addAttribute(HttpCodeView.CODE, HttpStatus.CREATED); // http 201 + + return ClientInformationResponseView.VIEWNAME; + } catch (IllegalArgumentException e) { + logger.error("Couldn't save client", e); + + m.addAttribute(JsonErrorView.ERROR, "invalid_client_metadata"); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, + "Unable to save client due to invalid or inconsistent metadata."); + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400 + + return JsonErrorView.VIEWNAME; + } + } else { + // didn't parse, this is a bad request + logger.error("registerNewClient failed; submitted JSON is malformed"); + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400 + + return HttpCodeView.VIEWNAME; + } + + } + + private ClientDetailsEntity validateScopes(ClientDetailsEntity newClient) + throws ValidationException { + // scopes that the client is asking for + Set requestedScopes = scopeService.fromStrings(newClient.getScope()); + + // the scopes that the client can have must be a subset of the dynamically allowed scopes + Set allowedScopes = + scopeService.removeRestrictedAndReservedScopes(requestedScopes); + + // if the client didn't ask for any, give them the defaults + if (allowedScopes == null || allowedScopes.isEmpty()) { + allowedScopes = scopeService.getDefaults(); + } + + newClient.setScope(scopeService.toStrings(allowedScopes)); + + return newClient; + } + + /** + * Get the meta information for a client. + * + * @param clientId + * @param m + * @param auth + * @return + */ + @PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + + SystemScopeService.RESOURCE_TOKEN_SCOPE + "')") + @RequestMapping(value = "/{id}", method = RequestMethod.GET, + produces = MediaType.APPLICATION_JSON_VALUE) + public String readResourceConfiguration(@PathVariable("id") String clientId, Model m, + OAuth2Authentication auth) { + + ClientDetailsEntity client = clientService.loadClientByClientId(clientId); + + if (client != null && client.getClientId().equals(auth.getOAuth2Request().getClientId())) { + + // possibly update the token + OAuth2AccessTokenEntity token = fetchValidRegistrationToken(auth, client); + + RegisteredClient registered = + new RegisteredClient(client, token.getValue(), config.getIssuer() + "resource/" + + UriUtils.encodePathSegment(client.getClientId(), "UTF-8")); + + // send it all out to the view + m.addAttribute("client", registered); + m.addAttribute(HttpCodeView.CODE, HttpStatus.OK); // http 200 + + return ClientInformationResponseView.VIEWNAME; + + } else { + // client mismatch + logger.error("readResourceConfiguration failed, client ID mismatch: " + clientId + " and " + + auth.getOAuth2Request().getClientId() + " do not match."); + m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); // http 403 + + return HttpCodeView.VIEWNAME; + } + } + + /** + * Update the metainformation for a given client. + * + * @param clientId + * @param jsonString + * @param m + * @param auth + * @return + */ + @PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + + SystemScopeService.RESOURCE_TOKEN_SCOPE + "')") + @RequestMapping(value = "/{id}", method = RequestMethod.PUT, + produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + public String updateProtectedResource(@PathVariable("id") String clientId, + @RequestBody String jsonString, Model m, OAuth2Authentication auth) { + + + ClientDetailsEntity newClient = null; + try { + newClient = ClientDetailsEntityJsonProcessor.parse(jsonString); + } catch (JsonSyntaxException e) { + // bad parse + // didn't parse, this is a bad request + logger.error("updateProtectedResource failed; submitted JSON is malformed"); + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400 + return HttpCodeView.VIEWNAME; + } + + ClientDetailsEntity oldClient = clientService.loadClientByClientId(clientId); + + if (newClient != null && oldClient != null // we have an existing client and the new one parsed + && oldClient.getClientId().equals(auth.getOAuth2Request().getClientId()) // the client + // passed in the + // URI matches the + // one in the auth + && oldClient.getClientId().equals(newClient.getClientId()) // the client passed in the body + // matches the one in the URI + ) { + + // a client can't ask to update its own client secret to any particular value + newClient.setClientSecret(oldClient.getClientSecret()); + + newClient.setCreatedAt(oldClient.getCreatedAt()); + + // no grant types are allowed + newClient.setGrantTypes(new HashSet()); + newClient.setResponseTypes(new HashSet()); + newClient.setRedirectUris(new HashSet()); + + // don't issue tokens to this client + newClient.setAccessTokenValiditySeconds(0); + newClient.setIdTokenValiditySeconds(0); + newClient.setRefreshTokenValiditySeconds(0); + + // clear out unused fields + newClient.setDefaultACRvalues(new HashSet()); + newClient.setDefaultMaxAge(null); + newClient.setIdTokenEncryptedResponseAlg(null); + newClient.setIdTokenEncryptedResponseEnc(null); + newClient.setIdTokenSignedResponseAlg(null); + newClient.setInitiateLoginUri(null); + newClient.setPostLogoutRedirectUris(null); + newClient.setRequestObjectSigningAlg(null); + newClient.setRequireAuthTime(null); + newClient.setReuseRefreshToken(false); + newClient.setSectorIdentifierUri(null); + newClient.setSubjectType(null); + newClient.setUserInfoEncryptedResponseAlg(null); + newClient.setUserInfoEncryptedResponseEnc(null); + newClient.setUserInfoSignedResponseAlg(null); + + // this client has been dynamically registered (obviously) + newClient.setDynamicallyRegistered(true); + + // this client has access to the introspection endpoint + newClient.setAllowIntrospection(true); + + // do validation on the fields + try { + newClient = validateScopes(newClient); + newClient = validateAuth(newClient); + } catch (ValidationException ve) { + // validation failed, return an error + m.addAttribute(JsonErrorView.ERROR, ve.getError()); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, ve.getErrorDescription()); + m.addAttribute(HttpCodeView.CODE, ve.getStatus()); + return JsonErrorView.VIEWNAME; + } + + + try { + // save the client + ClientDetailsEntity savedClient = clientService.updateClient(oldClient, newClient); + + // possibly update the token + OAuth2AccessTokenEntity token = fetchValidRegistrationToken(auth, savedClient); + + RegisteredClient registered = + new RegisteredClient(savedClient, token.getValue(), config.getIssuer() + "resource/" + + UriUtils.encodePathSegment(savedClient.getClientId(), "UTF-8")); + + // send it all out to the view + m.addAttribute("client", registered); + m.addAttribute(HttpCodeView.CODE, HttpStatus.OK); // http 200 + + return ClientInformationResponseView.VIEWNAME; + + } catch (IllegalArgumentException e) { + logger.error("Couldn't save client", e); + + m.addAttribute(JsonErrorView.ERROR, "invalid_client_metadata"); + m.addAttribute(JsonErrorView.ERROR_MESSAGE, + "Unable to save client due to invalid or inconsistent metadata."); + m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400 + + return JsonErrorView.VIEWNAME; + } + } else { + // client mismatch + logger.error("updateProtectedResource" + " failed, client ID mismatch: " + clientId + " and " + + auth.getOAuth2Request().getClientId() + " do not match."); + m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); // http 403 + + return HttpCodeView.VIEWNAME; + } + } + + /** + * Delete the indicated client from the system. + * + * @param clientId + * @param m + * @param auth + * @return + */ + @PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + + SystemScopeService.RESOURCE_TOKEN_SCOPE + "')") + @RequestMapping(value = "/{id}", method = RequestMethod.DELETE, + produces = MediaType.APPLICATION_JSON_VALUE) + public String deleteResource(@PathVariable("id") String clientId, Model m, + OAuth2Authentication auth) { + + ClientDetailsEntity client = clientService.loadClientByClientId(clientId); + + if (client != null && client.getClientId().equals(auth.getOAuth2Request().getClientId())) { + + clientService.deleteClient(client); + + m.addAttribute(HttpCodeView.CODE, HttpStatus.NO_CONTENT); // http 204 + + return HttpCodeView.VIEWNAME; + } else { + // client mismatch + logger.error("readClientConfiguration failed, client ID mismatch: " + clientId + " and " + + auth.getOAuth2Request().getClientId() + " do not match."); + m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); // http 403 + + return HttpCodeView.VIEWNAME; + } + } + + private ClientDetailsEntity validateAuth(ClientDetailsEntity newClient) + throws ValidationException { + if (newClient.getTokenEndpointAuthMethod() == null) { + newClient.setTokenEndpointAuthMethod(AuthMethod.SECRET_BASIC); + } + + if (newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_BASIC + || newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_JWT + || newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_POST) { + + if (Strings.isNullOrEmpty(newClient.getClientSecret())) { + // no secret yet, we need to generate a secret + newClient = clientService.generateClientSecret(newClient); + } + } else if (newClient.getTokenEndpointAuthMethod() == AuthMethod.PRIVATE_KEY) { + if (Strings.isNullOrEmpty(newClient.getJwksUri()) && newClient.getJwks() == null) { + throw new ValidationException("invalid_client_metadata", + "JWK Set URI required when using private key authentication", HttpStatus.BAD_REQUEST); + } + + newClient.setClientSecret(null); + } else if (newClient.getTokenEndpointAuthMethod() == AuthMethod.NONE) { + newClient.setClientSecret(null); + } else { + throw new ValidationException("invalid_client_metadata", "Unknown authentication method", + HttpStatus.BAD_REQUEST); + } + return newClient; + } + + private OAuth2AccessTokenEntity fetchValidRegistrationToken(OAuth2Authentication auth, + ClientDetailsEntity client) { + + OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) auth.getDetails(); + OAuth2AccessTokenEntity token = tokenService.readAccessToken(details.getTokenValue()); + + if (config.getRegTokenLifeTime() != null) { + + try { + // Re-issue the token if it has been issued before [currentTime - validity] + Date validToDate = + new Date(System.currentTimeMillis() - config.getRegTokenLifeTime() * 1000); + if (token.getJwt().getJWTClaimsSet().getIssueTime().before(validToDate)) { + logger.info("Rotating the registration access token for " + client.getClientId()); + tokenService.revokeAccessToken(token); + OAuth2AccessTokenEntity newToken = connectTokenService.createResourceAccessToken(client); + tokenService.saveAccessToken(newToken); + return newToken; + } else { + // it's not expired, keep going + return token; + } + } catch (ParseException e) { + logger.error("Couldn't parse a known-valid token?", e); + return token; + } + } else { + // tokens don't expire, just return it + return token; + } + } } diff --git a/pom.xml b/pom.xml index d3f3e3ad8..e9288f06f 100644 --- a/pom.xml +++ b/pom.xml @@ -385,35 +385,36 @@ org.springframework spring-framework-bom - 4.3.7.RELEASE + 5.3.12 pom import + com.fasterxml.jackson.core jackson-databind - 2.9.0.pr2 + 2.12.5 com.fasterxml.jackson.core jackson-annotations - 2.9.0.pr2 + 2.12.5 org.springframework.security spring-security-bom - 4.2.4.RELEASE + 5.5.3 pom import org.springframework.security.oauth spring-security-oauth2 - 2.1.0.RELEASE + 2.5.1.RELEASE