diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ClientAPI.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ClientAPI.java index bf4df650d..e5f2709ff 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ClientAPI.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ClientAPI.java @@ -18,6 +18,9 @@ package org.mitre.openid.connect.web; import java.lang.reflect.Type; import java.util.Collection; +import org.mitre.jose.JWEAlgorithmEmbed; +import org.mitre.jose.JWEEncryptionMethodEmbed; +import org.mitre.jose.JWSAlgorithmEmbed; import org.mitre.oauth2.model.ClientDetailsEntity; import org.mitre.oauth2.service.ClientDetailsEntityService; import org.slf4j.Logger; diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ClientDynamicRegistrationEndpoint.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ClientDynamicRegistrationEndpoint.java index b0e6422e6..3e06ec678 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ClientDynamicRegistrationEndpoint.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/ClientDynamicRegistrationEndpoint.java @@ -2,12 +2,12 @@ package org.mitre.openid.connect.web; -import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; -import org.mitre.jose.JWSAlgorithmEntity; -import org.mitre.oauth2.exception.ClientNotFoundException; +import org.mitre.jose.JWEAlgorithmEmbed; +import org.mitre.jose.JWEEncryptionMethodEmbed; +import org.mitre.jose.JWSAlgorithmEmbed; import org.mitre.oauth2.model.ClientDetailsEntity; import org.mitre.oauth2.model.ClientDetailsEntity.AppType; import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod; @@ -24,23 +24,27 @@ import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.oauth2.common.exceptions.InvalidClientException; -import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; -import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; import org.springframework.security.oauth2.provider.DefaultAuthorizationRequest; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails; import org.springframework.stereotype.Controller; -import org.springframework.ui.ModelMap; +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.RequestParam; +import org.springframework.web.bind.annotation.RequestMethod; -import com.google.common.base.Strings; +import com.google.common.base.Splitter; import com.google.common.collect.Sets; -import com.nimbusds.jose.JWSAlgorithm; +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; +import com.google.gson.reflect.TypeToken; @Controller -@RequestMapping(value = "register"/*, method = RequestMethod.POST*/) +@RequestMapping(value = "register") public class ClientDynamicRegistrationEndpoint { @Autowired @@ -53,127 +57,409 @@ public class ClientDynamicRegistrationEndpoint { private SystemScopeService scopeService; private Logger logger = LoggerFactory.getLogger(this.getClass()); + private JsonParser parser = new JsonParser(); + private Gson gson = new Gson(); - @RequestMapping(params = "operation=client_register", produces = "application/json") - public String clientRegister( - @RequestParam(value = "redirect_uris", required = true) Set redirectUris, - @RequestParam(value = "client_name", required = false) String clientName, - @RequestParam(value = "client_url", required = false) String clientUrl, - @RequestParam(value = "logo_url", required = false) String logoUrl, - @RequestParam(value = "contacts", required = false) Set contacts, - @RequestParam(value = "tos_url", required = false) String tosUrl, - @RequestParam(value = "token_endpoint_auth_method", required = false) AuthMethod tokenEndpointAuthMethod, - @RequestParam(value = "policy_url", required = false) String policyUrl, - - @RequestParam(value = "scope", required = false) Set scope, - @RequestParam(value = "grant_type", required = false) Set grantType, - - @RequestParam(value = "jwk_url", required = false) String jwkUrl, - @RequestParam(value = "jwk_encryption_url", required = false) String jwkEncryptionUrl, - @RequestParam(value = "x509_url", required = false) String x509Url, - @RequestParam(value = "x509_encryption_url", required = false) String x509EncryptionUrl, - @RequestParam(value = "default_max_age", required = false) Integer defaultMaxAge, - @RequestParam(value = "default_acr", required = false) String defaultAcr, - - // OPENID CONNECT EXTENSIONS BELOW - @RequestParam(value = "application_type", required = false) AppType applicationType, - @RequestParam(value = "sector_identifier_url", required = false) String sectorIdentifierUrl, - @RequestParam(value = "subject_type", required = false) SubjectType subjectType, - @RequestParam(value = "require_signed_request_object", required = false) JWSAlgorithm requireSignedRequestObject, - // TODO: JWE needs to be handled properly, see @InitBinder above -- we'll ignore these right now - /* - @RequestParam(value = "userinfo_signed_response_alg", required = false) String userinfoSignedResponseAlg, - @RequestParam(value = "userinfo_encrypted_response_alg", required = false) String userinfoEncryptedResponseAlg, - @RequestParam(value = "userinfo_encrypted_response_enc", required = false) String userinfoEncryptedResponseEnc, - @RequestParam(value = "userinfo_encrypted_response_int", required = false) String userinfoEncryptedResponseInt, - @RequestParam(value = "idtoken_signed_response_alg", required = false) String idtokenSignedResponseAlg, - @RequestParam(value = "idtoken_encrypted_response_alg", required = false) String idtokenEncryptedResponseAlg, - @RequestParam(value = "idtoken_encrypted_response_enc", required = false) String idtokenEncryptedResponseEnc, - @RequestParam(value = "idtoken_encrypted_response_int", required = false) String idtokenEncryptedResponseInt, - */ - - @RequestParam(value = "require_auth_time", required = false, defaultValue = "true") Boolean requireAuthTime, - ModelMap model - ) { + /** + * 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 = "application/json", produces = "application/json") + public String registerNewClient(@RequestBody String jsonString, Model m) { + ClientDetailsEntity newClient = parse(jsonString); - // Create a new Client - - ClientDetailsEntity client = new ClientDetailsEntity(); + 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); + + // set of scopes that are OK for clients to dynamically register for + Set dynScopes = scopeService.getDynReg(); - // if it's not using a private key or no auth, then generate a secret - if (tokenEndpointAuthMethod != AuthMethod.PRIVATE_KEY && tokenEndpointAuthMethod != AuthMethod.NONE) { - client = clientService.generateClientSecret(client); - } - - client.setContacts(contacts); - client.setApplicationType(applicationType); - client.setClientName(clientName); - client.setClientUrl(clientUrl); - client.setTosUrl(tosUrl); - client.setLogoUrl(logoUrl); - client.setRegisteredRedirectUri(redirectUris); - client.setTokenEndpointAuthMethod(tokenEndpointAuthMethod); - client.setPolicyUrl(policyUrl); - client.setJwkUrl(jwkUrl); - client.setJwkEncryptionUrl(jwkEncryptionUrl); - client.setX509Url(x509Url); - client.setX509EncryptionUrl(x509EncryptionUrl); - client.setSectorIdentifierUrl(sectorIdentifierUrl); - client.setSubjectType(subjectType); - client.setRequireSignedRequestObject(new JWSAlgorithmEntity(requireSignedRequestObject)); - client.setDefaultMaxAge(defaultMaxAge); - client.setRequireAuthTime(requireAuthTime == null ? false : requireAuthTime.booleanValue()); - client.setDefaultACR(defaultAcr); + // scopes that the client is asking for + Set requestedScopes = scopeService.fromStrings(newClient.getScope()); - // set of scopes that are OK for clients to dynamically register for - Set dynScopes = scopeService.getDynReg(); - - // scopes that the client is asking for - Set requestedScopes = scopeService.fromStrings(scope); - if (requestedScopes == null) { - requestedScopes = scopeService.getDefaults(); - } - - // the scopes that the client can have must be a subset of the dynamically allowed scopes - Set allowedScopes = Sets.intersection(dynScopes, requestedScopes); + // if the client didn't ask for any, give them the defaults + if (requestedScopes == null || requestedScopes.isEmpty()) { + requestedScopes = scopeService.getDefaults(); + } - client.setScope(scopeService.toStrings(allowedScopes)); - - - - if (grantType != null) { - // TODO: check against some kind of grant type service for validity - client.setAuthorizedGrantTypes(grantType); + // the scopes that the client can have must be a subset of the dynamically allowed scopes + Set allowedScopes = Sets.intersection(dynScopes, requestedScopes); + + newClient.setScope(scopeService.toStrings(allowedScopes)); + + + // set default grant types if needed + if (newClient.getGrantTypes() == null || newClient.getGrantTypes().isEmpty()) { + newClient.setGrantTypes(Sets.newHashSet("authorization_code", "refresh_token")); // allow authorization code and refresh token grant types by default + } + + // set default response types if needed + // TODO: these aren't checked by SECOAUTH + // TODO: the consistency between the response_type and grant_type needs to be checked by the client service, most likely + if (newClient.getResponseTypes() == null || newClient.getResponseTypes().isEmpty()) { + newClient.setResponseTypes(Sets.newHashSet("code")); // default to allowing only the auth code flow + } + + 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) { + + // we need to generate a secret + newClient = clientService.generateClientSecret(newClient); + } + + // set some defaults for token timeouts + newClient.setAccessTokenValiditySeconds((int)TimeUnit.HOURS.toSeconds(1)); // access tokens good for 1hr + newClient.setIdTokenValiditySeconds((int)TimeUnit.MINUTES.toSeconds(10)); // id tokens good for 10min + newClient.setRefreshTokenValiditySeconds(null); // refresh tokens good until revoked + + // this client has been dynamically registered (obviously) + newClient.setDynamicallyRegistered(true); + + // now save it + ClientDetailsEntity savedClient = clientService.saveNewClient(newClient); + + // generate the registration access token + OAuth2AccessTokenEntity token = createRegistrationAccessToken(savedClient); + + // send it all out to the view + m.addAttribute("client", savedClient); + m.addAttribute("code", HttpStatus.CREATED); // http 201 + m.addAttribute("token", token); + + return "clientInformationResponseView"; } else { - client.setAuthorizedGrantTypes(Sets.newHashSet("authorization_code", "refresh_token")); // allow authorization code and refresh token grant types + // didn't parse, this is a bad request + logger.error("ClientDynamicRegistrationEndpoint: registerNewClient failed; submitted JSON is malformed"); + m.addAttribute("code", HttpStatus.BAD_REQUEST); // http 400 + + return "httpCodeView"; } - // defaults for SECOAUTH functionality - // TODO: extensions to request, or configuration? - client.setAccessTokenValiditySeconds((int)TimeUnit.HOURS.toSeconds(1)); // access tokens good for 1hr - client.setIdTokenValiditySeconds((int)TimeUnit.MINUTES.toSeconds(10)); // id tokens good for 10min - client.setRefreshTokenValiditySeconds(null); // refresh tokens good until revoked - - client.setDynamicallyRegistered(true); - - ClientDetailsEntity saved = clientService.saveNewClient(client); - - OAuth2AccessTokenEntity registrationAccessToken = createRegistrationAccessToken(client); - - model.put("fullClient", Boolean.TRUE); - model.put("client", saved); - model.put("token", registrationAccessToken); - - return "clientRegistration"; } /** + * Get the meta information for a client. + * @param clientId + * @param m + * @param auth + * @return + */ + @PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + OAuth2AccessTokenEntity.REGISTRATION_TOKEN_SCOPE + "')") + @RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = "application/json") + public String readClientConfiguration(@PathVariable("id") String clientId, Model m, OAuth2Authentication auth) { + + ClientDetailsEntity client = clientService.loadClientByClientId(clientId); + + if (client != null && client.getClientId().equals(auth.getAuthorizationRequest().getClientId())) { + + + // we return the token that we got in + OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) auth.getDetails(); + OAuth2AccessTokenEntity token = tokenService.readAccessToken(details.getTokenValue()); + + // send it all out to the view + m.addAttribute("client", client); + m.addAttribute("code", HttpStatus.OK); // http 200 + m.addAttribute("token", token); + + return "clientInformationResponseView"; + } else { + // client mismatch + logger.error("ClientDynamicRegistrationEndpoint: readClientConfiguration failed, client ID mismatch: " + + clientId + " and " + auth.getAuthorizationRequest().getClientId() + " do not match."); + m.addAttribute("code", HttpStatus.FORBIDDEN); // http 403 + + return "httpCodeView"; + } + } + + /** + * Update the metainformation for a given client. + * @param clientId + * @param jsonString + * @param m + * @param auth + * @return + */ + @PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + OAuth2AccessTokenEntity.REGISTRATION_TOKEN_SCOPE + "')") + @RequestMapping(value = "/{id}", method = RequestMethod.PUT, produces = "application/json", consumes = "application/json") + public String updateClient(@PathVariable("id") String clientId, @RequestBody String jsonString, Model m, OAuth2Authentication auth) { + + + ClientDetailsEntity newClient = parse(jsonString); + ClientDetailsEntity oldClient = clientService.loadClientByClientId(clientId); + + if (newClient != null && oldClient != null // we have an existing client and the new one parsed + && oldClient.getClientId().equals(auth.getAuthorizationRequest().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()); + + // we need to copy over all of the local and SECOAUTH fields + newClient.setAccessTokenValiditySeconds(oldClient.getAccessTokenValiditySeconds()); + newClient.setIdTokenValiditySeconds(oldClient.getIdTokenValiditySeconds()); + newClient.setRefreshTokenValiditySeconds(oldClient.getRefreshTokenValiditySeconds()); + newClient.setDynamicallyRegistered(true); // it's still dynamically registered + newClient.setAllowIntrospection(oldClient.isAllowIntrospection()); + newClient.setAuthorities(oldClient.getAuthorities()); + newClient.setClientDescription(oldClient.getClientDescription()); + newClient.setCreatedAt(oldClient.getCreatedAt()); + newClient.setReuseRefreshToken(oldClient.isReuseRefreshToken()); + + // set of scopes that are OK for clients to dynamically register for + Set dynScopes = scopeService.getDynReg(); + + // 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 = Sets.intersection(dynScopes, requestedScopes); + + // make sure that the client doesn't ask for scopes it can't have + newClient.setScope(scopeService.toStrings(allowedScopes)); + + // save the client + ClientDetailsEntity savedClient = clientService.updateClient(oldClient, newClient); + + // we return the token that we got in + // TODO: rotate this after some set amount of time + OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) auth.getDetails(); + OAuth2AccessTokenEntity token = tokenService.readAccessToken(details.getTokenValue()); + + // send it all out to the view + m.addAttribute("client", savedClient); + m.addAttribute("code", HttpStatus.OK); // http 200 + m.addAttribute("token", token); + + return "clientInformationResponseView"; + } else { + // client mismatch + logger.error("ClientDynamicRegistrationEndpoint: readClientConfiguration failed, client ID mismatch: " + + clientId + " and " + auth.getAuthorizationRequest().getClientId() + " do not match."); + m.addAttribute("code", HttpStatus.FORBIDDEN); // http 403 + + return "httpCodeView"; + } + } + + /** + * Delete the indicated client from the system. + * @param clientId + * @param m + * @param auth + * @return + */ + @PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + OAuth2AccessTokenEntity.REGISTRATION_TOKEN_SCOPE + "')") + @RequestMapping(value = "/{id}", method = RequestMethod.DELETE, produces = "application/json") + public String deleteClient(@PathVariable("id") String clientId, Model m, OAuth2Authentication auth) { + + ClientDetailsEntity client = clientService.loadClientByClientId(clientId); + + if (client != null && client.getClientId().equals(auth.getAuthorizationRequest().getClientId())) { + + clientService.deleteClient(client); + + // we return the token that we got in + OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) auth.getDetails(); + OAuth2AccessTokenEntity token = tokenService.readAccessToken(details.getTokenValue()); + + // send it all out to the view + m.addAttribute("client", client); + m.addAttribute("code", HttpStatus.OK); // http 200 + m.addAttribute("token", token); + + return "clientInformationResponseView"; + } else { + // client mismatch + logger.error("ClientDynamicRegistrationEndpoint: readClientConfiguration failed, client ID mismatch: " + + clientId + " and " + auth.getAuthorizationRequest().getClientId() + " do not match."); + m.addAttribute("code", HttpStatus.FORBIDDEN); // http 403 + + return "httpCodeView"; + } + } + + + + + /** + * + * Create an unbound ClientDetailsEntity from the given JSON string. + * + * @param jsonString + * @return the entity if successful, null otherwise + */ + private ClientDetailsEntity parse(String jsonString) { + JsonElement jsonEl = parser.parse(jsonString); + if (jsonEl.isJsonObject()) { + + JsonObject o = jsonEl.getAsJsonObject(); + ClientDetailsEntity c = new ClientDetailsEntity(); + + // TODO: make these field names into constants + + // these two fields should only be sent in the update request, and MUST match existing values + c.setClientId(getAsString(o, "client_id")); + c.setClientSecret(getAsString(o, "client_secret")); + + // OAuth DynReg + c.setRedirectUris(getAsStringSet(o, "redirect_uris")); + c.setClientName(getAsString(o, "client_name")); + c.setClientUri(getAsString(o, "client_uri")); + c.setLogoUri(getAsString(o, "logo_uri")); + c.setContacts(getAsStringSet(o, "contacts")); + c.setTosUri(getAsString(o, "tos_uri")); + + String authMethod = getAsString(o, "token_endpoint_auth_method"); + if (authMethod != null) { + c.setTokenEndpointAuthMethod(AuthMethod.getByValue(authMethod)); + } + + // scope is a space-separated string + String scope = getAsString(o, "scope"); + if (scope != null) { + c.setScope(Sets.newHashSet(Splitter.on(" ").split(scope))); + } + + c.setGrantTypes(getAsStringSet(o, "grant_types")); + c.setPolicyUri(getAsString(o, "policy_uri")); + c.setJwksUri(getAsString(o, "jwks_uri")); + + + // OIDC Additions + String appType = getAsString(o, "application_type"); + if (appType != null) { + c.setApplicationType(AppType.getByValue(appType)); + } + + c.setSectorIdentifierUri(getAsString(o, "sector_identifier_uri")); + + String subjectType = getAsString(o, "subject_type"); + if (subjectType != null) { + c.setSubjectType(SubjectType.getByValue(subjectType)); + } + + c.setRequestObjectSigningAlg(getAsJwsAlgorithm(o, "request_object_signing_alg")); + + c.setUserInfoSignedResponseAlg(getAsJwsAlgorithm(o, "userinfo_signed_response_alg")); + c.setUserInfoEncryptedResponseAlg(getAsJweAlgorithm(o, "userinfo_encrypted_response_alg")); + c.setUserInfoEncryptedResponseEnc(getAsJweEncryptionMethod(o, "userinfo_encrypted_response_enc")); + + c.setIdTokenSignedResponseAlg(getAsJwsAlgorithm(o, "id_token_signed_response_alg")); + c.setIdTokenEncryptedResponseAlg(getAsJweAlgorithm(o, "id_token_encrypted_response_alg")); + c.setIdTokenEncryptedResponseEnc(getAsJweEncryptionMethod(o, "id_token_encrypted_response_enc")); + + if (o.has("default_max_age")) { + if (o.get("default_max_age").isJsonPrimitive()) { + c.setDefaultMaxAge(o.get("default_max_age").getAsInt()); + } + } + + if (o.has("require_auth_time")) { + if (o.get("require_auth_time").isJsonPrimitive()) { + c.setRequireAuthTime(o.get("require_auth_time").getAsBoolean()); + } + } + + c.setDefaultACRvalues(getAsStringSet(o, "default_acr_values")); + c.setInitiateLoginUri(getAsString(o, "initiate_login_uri")); + c.setPostLogoutRedirectUri(getAsString(o, "post_logout_redirect_uri")); + c.setRequestUris(getAsStringSet(o, "request_uris")); + + return c; + } else { + return null; + } + } + + /** + * Gets the value of the given given member as a set of strings, null if it doesn't exist + */ + private Set getAsStringSet(JsonObject o, String member) throws JsonSyntaxException { + if (o.has(member)) { + return gson.fromJson(o.get(member), new TypeToken>(){}.getType()); + } else { + return null; + } + } + + /** + * Gets the value of the given member as a string, null if it doesn't exist + */ + private String getAsString(JsonObject o, String member) { + if (o.has(member)) { + JsonElement e = o.get(member); + if (e != null && e.isJsonPrimitive()) { + return e.getAsString(); + } else { + return null; + } + } else { + return null; + } + } + + /** + * Gets the value of the given member as a JWS Algorithm, null if it doesn't exist + */ + private JWSAlgorithmEmbed getAsJwsAlgorithm(JsonObject o, String member) { + String s = getAsString(o, member); + if (s != null) { + return JWSAlgorithmEmbed.getForAlgorithmName(s); + } else { + return null; + } + } + + /** + * Gets the value of the given member as a JWE Algorithm, null if it doesn't exist + */ + private JWEAlgorithmEmbed getAsJweAlgorithm(JsonObject o, String member) { + String s = getAsString(o, member); + if (s != null) { + return JWEAlgorithmEmbed.getForAlgorithmName(s); + } else { + return null; + } + } + + + /** + * Gets the value of the given member as a JWE Encryption Method, null if it doesn't exist + */ + private JWEEncryptionMethodEmbed getAsJweEncryptionMethod(JsonObject o, String member) { + String s = getAsString(o, member); + if (s != null) { + return JWEEncryptionMethodEmbed.getForAlgorithmName(s); + } else { + return null; + } + } + /** * @param client * @return * @throws AuthenticationException */ - private OAuth2AccessTokenEntity createRegistrationAccessToken(ClientDetailsEntity client) throws AuthenticationException, InvalidClientException { + private OAuth2AccessTokenEntity createRegistrationAccessToken(ClientDetailsEntity client) throws AuthenticationException { // create a registration access token, treat it like a client credentials flow // I can't use the auth request interface here because it has no setters and bad constructors -- THIS IS BAD API DESIGN DefaultAuthorizationRequest authorizationRequest = new DefaultAuthorizationRequest(client.getClientId(), Sets.newHashSet(OAuth2AccessTokenEntity.REGISTRATION_TOKEN_SCOPE)); @@ -184,223 +470,4 @@ public class ClientDynamicRegistrationEndpoint { return registrationAccessToken; } - @PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('registration-token')") - @RequestMapping(params = "operation=rotate_secret", produces = "application/json") - public String rotateSecret(OAuth2Authentication auth, ModelMap model) { - - - String clientId = auth.getAuthorizationRequest().getClientId(); - ClientDetailsEntity client = clientService.loadClientByClientId(clientId); - - if (client == null) { - logger.error("ClientDynamicRegistrationEndpoint: rotateSecret failed, could not find client " + clientId); - model.addAttribute("code", HttpStatus.NOT_FOUND); - return "httpCodeView"; - } - - // rotate the secret, if available - if (client.isSecretRequired()) { - client = clientService.generateClientSecret(client); - } - - OAuth2AccessTokenEntity registrationAccessToken = null; - - try { - // mint a new access token - registrationAccessToken = createRegistrationAccessToken(client); - } catch (AuthenticationException e) { - logger.error("ClientDynamicRegistrationEndpoint: rotateSecret failed; AuthenticationException: CLient " + clientId - + " attempted to rotate secret and failed with the following stack trace: " - + e.getStackTrace().toString()); - model.addAttribute("code", HttpStatus.FORBIDDEN); - return "httpCodeView"; - } - - // revoke the old one - OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) auth.getDetails(); - if (details != null) { - OAuth2AccessTokenEntity oldAccessToken = null; - - try { - oldAccessToken = tokenService.readAccessToken(details.getTokenValue()); - } catch (AuthenticationException e) { - logger.error("ClientDynamicRegistrationEndpoint: rotateSecret failed; AuthenticationException: CLient " + clientId - + " attempted to rotate secret and failed with the following stack trace: " - + e.getStackTrace().toString()); - model.addAttribute("code", HttpStatus.FORBIDDEN); - return "httpCodeView"; - } catch (InvalidTokenException e) { - logger.error("ClientDynamicRegistrationEndpoint: rotateSecret failed; InvalidTokenException: CLient " + clientId - + " attempted to rotate secret with an invalid token." - + e.getStackTrace().toString()); - model.addAttribute("code", HttpStatus.BAD_REQUEST); - return "httpCodeView"; - } - if (oldAccessToken != null) { - tokenService.revokeAccessToken(oldAccessToken); - } else { - // This is a severe error - logger.error("SEVERE: ClientDynamicRegistrationEndpoint: rotateSecret failed; Revocation of access token for client " + clientId - + " failed. Original token can not be found."); - throw OAuth2Exception.create(OAuth2Exception.INVALID_TOKEN, "SEVERE: token not found, something is fishy"); - } - } - - // save the client - ClientDetailsEntity saved = clientService.updateClient(client, client); - - model.put("fullClient", Boolean.FALSE); - model.put("client", saved); - model.put("token", registrationAccessToken); - - return "clientRegistration"; - } - - @PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('registration-token')") - @RequestMapping(params = "operation=client_update", produces = "application/json") - public String clientUpdate( - @RequestParam(value = "redirect_uris", required = true) Set redirectUris, - @RequestParam(value = "client_name", required = false) String clientName, - @RequestParam(value = "client_url", required = false) String clientUrl, - @RequestParam(value = "logo_url", required = false) String logoUrl, - @RequestParam(value = "contacts", required = false) Set contacts, - @RequestParam(value = "tos_url", required = false) String tosUrl, - @RequestParam(value = "token_endpoint_auth_method", required = false) AuthMethod tokenEndpointAuthMethod, - @RequestParam(value = "policy_url", required = false) String policyUrl, - - @RequestParam(value = "scope", required = false) Set scope, - @RequestParam(value = "grant_type", required = false) Set grantType, - - @RequestParam(value = "jwk_url", required = false) String jwkUrl, - @RequestParam(value = "jwk_encryption_url", required = false) String jwkEncryptionUrl, - @RequestParam(value = "x509_url", required = false) String x509Url, - @RequestParam(value = "x509_encryption_url", required = false) String x509EncryptionUrl, - @RequestParam(value = "default_max_age", required = false) Integer defaultMaxAge, - @RequestParam(value = "default_acr", required = false) String defaultAcr, - - // OPENID CONNECT EXTENSIONS BELOW - @RequestParam(value = "application_type", required = false) AppType applicationType, - @RequestParam(value = "sector_identifier_url", required = false) String sectorIdentifierUrl, - @RequestParam(value = "subject_type", required = false) SubjectType subjectType, - @RequestParam(value = "require_signed_request_object", required = false) JWSAlgorithm requireSignedRequestObject, - @RequestParam(value = "require_auth_time", required = false, defaultValue = "true") Boolean requireAuthTime, - // TODO: JWE needs to be handled properly, see @InitBinder above -- we'll ignore these right now - /* - @RequestParam(value = "userinfo_signed_response_alg", required = false) String userinfoSignedResponseAlg, - @RequestParam(value = "userinfo_encrypted_response_alg", required = false) String userinfoEncryptedResponseAlg, - @RequestParam(value = "userinfo_encrypted_response_enc", required = false) String userinfoEncryptedResponseEnc, - @RequestParam(value = "userinfo_encrypted_response_int", required = false) String userinfoEncryptedResponseInt, - @RequestParam(value = "idtoken_signed_response_alg", required = false) String idtokenSignedResponseAlg, - @RequestParam(value = "idtoken_encrypted_response_alg", required = false) String idtokenEncryptedResponseAlg, - @RequestParam(value = "idtoken_encrypted_response_enc", required = false) String idtokenEncryptedResponseEnc, - @RequestParam(value = "idtoken_encrypted_response_int", required = false) String idtokenEncryptedResponseInt, - */ - - @RequestParam Map params, - - OAuth2Authentication auth, - ModelMap model - - ) { - - String clientId = auth.getAuthorizationRequest().getClientId(); - ClientDetailsEntity client = clientService.loadClientByClientId(clientId); - - if (client == null) { - logger.error("ClientDynamicRegistrationEndpoint: clientUpdate failed; Client with id " + clientId + " does not exist or cannot be found."); - model.addAttribute("code", HttpStatus.NOT_FOUND); - return "httpCodeView"; - } - - /* - * now process each field: - * 1) If input is not provided (null, not in map), keep existing value - * 2) If input is provided (in map) but null or blank, remove existing value - * 3) If input is not null and not blank, replace existing value - */ - if (params.containsKey("contacts")) { - client.setContacts(contacts); - } - if (params.containsKey("application_type")) { - client.setApplicationType(applicationType); - } - if (params.containsKey("client_name")) { - client.setClientName(Strings.emptyToNull(clientName)); - } - if (params.containsKey("client_url")) { - client.setClientUrl(Strings.emptyToNull(clientUrl)); - } - if (params.containsKey("tos_url")) { - client.setTosUrl(Strings.emptyToNull(tosUrl)); - } - if (params.containsKey("logo_url")) { - client.setLogoUrl(Strings.emptyToNull(logoUrl)); - } - if (params.containsKey("redirect_uris")) { - client.setRegisteredRedirectUri(redirectUris); - } - if (params.containsKey("token_endpoint_auth_method")) { - client.setTokenEndpointAuthMethod(tokenEndpointAuthMethod); - } - if (params.containsKey("policy_url")) { - client.setPolicyUrl(Strings.emptyToNull(policyUrl)); - } - if (params.containsKey("jwk_url")) { - client.setJwkUrl(Strings.emptyToNull(jwkUrl)); - } - if (params.containsKey("jwk_encryption_url")) { - client.setJwkEncryptionUrl(Strings.emptyToNull(jwkEncryptionUrl)); - } - if (params.containsKey("x509_url")) { - client.setX509Url(Strings.emptyToNull(x509Url)); - } - if (params.containsKey("x509_encryption_url")) { - client.setX509EncryptionUrl(Strings.emptyToNull(x509EncryptionUrl)); - } - if (params.containsKey("default_max_age")) { - client.setDefaultMaxAge(defaultMaxAge); - } - if (params.containsKey("default_acr")) { - client.setDefaultACR(Strings.emptyToNull(defaultAcr)); - } - if (params.containsKey("scope")) { - // set of scopes that are OK for clients to dynamically register for - Set dynScopes = scopeService.getDynReg(); - - // scopes that the client is asking for - Set requestedScopes = scopeService.fromStrings(scope); - - // the scopes that the client can have must be a subset of the dynamically allowed scopes - Set allowedScopes = Sets.intersection(dynScopes, requestedScopes); - - client.setScope(scopeService.toStrings(allowedScopes)); - } - if (params.containsKey("grant_type")) { - // TODO: check against some kind of grant type service for validity - client.setAuthorizedGrantTypes(grantType); - } - - - // OIDC - if (params.containsKey("sector_identifier_url")) { - client.setSectorIdentifierUrl(Strings.emptyToNull(sectorIdentifierUrl)); - } - if (params.containsKey("subject_type")) { - client.setSubjectType(subjectType); - } - if (params.containsKey("require_signed_request_object")) { // TODO: rename field - client.setRequireSignedRequestObject(new JWSAlgorithmEntity(requireSignedRequestObject)); - } - if (params.containsKey("require_auth_time")) { - client.setRequireAuthTime(requireAuthTime == null ? false : requireAuthTime.booleanValue()); // watch out for autoboxing - } - - - ClientDetailsEntity saved = clientService.updateClient(client, client); - - model.put("fullClient", Boolean.TRUE); - model.put("client", saved); - return "clientRegister"; - } - -} +} \ No newline at end of file