From deaccf437e433504654e86ead988d924dca2628b Mon Sep 17 00:00:00 2001 From: Justin Richer Date: Fri, 6 Jun 2014 10:18:40 -0400 Subject: [PATCH] refactored dynamic registration endpoint's checks for client consistency --- .../ClientDynamicRegistrationEndpoint.java | 516 ++++++++---------- 1 file changed, 218 insertions(+), 298 deletions(-) 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 41e33a933..43fd5fe0c 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 @@ -112,169 +112,21 @@ public class ClientDynamicRegistrationEndpoint { newClient.setClientId(null); newClient.setClientSecret(null); - // 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); - - // 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)); - - - // set default grant types if needed - if (newClient.getGrantTypes() == null || newClient.getGrantTypes().isEmpty()) { - if (newClient.getScope().contains("offline_access")) { // client asked for offline access - newClient.setGrantTypes(Sets.newHashSet("authorization_code", "refresh_token")); // allow authorization code and refresh token grant types by default - } else { - newClient.setGrantTypes(Sets.newHashSet("authorization_code")); // allow authorization code grant type by default - } - } - - if (newClient.getResponseTypes() == null) { - newClient.setResponseTypes(new HashSet()); - } - - // filter out unknown grant types - // TODO: make this a pluggable service - Set requestedGrantTypes = new HashSet(newClient.getGrantTypes()); - requestedGrantTypes.removeAll( - ImmutableSet.of("authorization_code", "implicit", - "password", "client_credentials", "refresh_token", - "urn:ietf:params:oauth:grant_type:redelegate")); - if (!requestedGrantTypes.isEmpty()) { - // return an error, there were unknown grant types requested - m.addAttribute("error", "invalid_client_metadata"); - m.addAttribute("errorMessage", "Unknown grant types requested: " + newClient.getGrantTypes()); - m.addAttribute("code", HttpStatus.BAD_REQUEST); - return "jsonErrorView"; - } - - // don't allow "password" grant type for dynamic registration - if (newClient.getGrantTypes().contains("password")) { - // return an error, you can't dynamically register for the password grant - m.addAttribute("error", "invalid_client_metadata"); - m.addAttribute("errorMessage", "The password grant type is not allowed in dynamic registration on this server."); - m.addAttribute("code", HttpStatus.BAD_REQUEST); + // do validation on the fields + try { + newClient = validateScopes(newClient); + newClient = validateResponseTypes(newClient); + newClient = validateGrantTypes(newClient); + newClient = validateRedirectUris(newClient); + newClient = validateAuth(newClient); + } catch (ValidationException ve) { + // validation failed, return an error + m.addAttribute("error", ve.getError()); + m.addAttribute("errorMessage", ve.getErrorDescription()); + m.addAttribute("code", ve.getStatus()); return "jsonErrorView"; } - - // don't allow clients to have multiple incompatible grant types and scopes - if (newClient.getGrantTypes().contains("authorization_code")) { - - // check for incompatible grants - if (newClient.getGrantTypes().contains("implicit") || - newClient.getGrantTypes().contains("client_credentials")) { - // return an error, you can't have these grant types together - m.addAttribute("error", "invalid_client_metadata"); - m.addAttribute("errorMessage", "Incompatible grant types requested: " + newClient.getGrantTypes()); - m.addAttribute("code", HttpStatus.BAD_REQUEST); - return "jsonErrorView"; - } - - if (newClient.getResponseTypes().contains("token")) { - // return an error, you can't have this grant type and response type together - m.addAttribute("error", "invalid_client_metadata"); - m.addAttribute("errorMessage", "Incompatible response types requested: " + newClient.getGrantTypes() + " / " + newClient.getResponseTypes()); - m.addAttribute("code", HttpStatus.BAD_REQUEST); - return "jsonErrorView"; - } - - newClient.getResponseTypes().add("code"); - - - } - - if (newClient.getGrantTypes().contains("implicit")) { - - // check for incompatible grants - if (newClient.getGrantTypes().contains("authorization_code") || - newClient.getGrantTypes().contains("client_credentials")) { - // return an error, you can't have these grant types together - m.addAttribute("error", "invalid_client_metadata"); - m.addAttribute("errorMessage", "Incompatible grant types requested: " + newClient.getGrantTypes()); - m.addAttribute("code", HttpStatus.BAD_REQUEST); - return "jsonErrorView"; - } - - if (newClient.getResponseTypes().contains("code")) { - // return an error, you can't have this grant type and response type together - m.addAttribute("error", "invalid_client_metadata"); - m.addAttribute("errorMessage", "Incompatible response types requested: " + newClient.getGrantTypes() + " / " + newClient.getResponseTypes()); - m.addAttribute("code", HttpStatus.BAD_REQUEST); - return "jsonErrorView"; - } - - newClient.getResponseTypes().add("token"); - - // don't allow refresh tokens in implicit clients - newClient.getGrantTypes().remove("refresh_token"); - newClient.getScope().remove("offline_access"); - } - - if (newClient.getGrantTypes().contains("client_credentials")) { - - // check for incompatible grants - if (newClient.getGrantTypes().contains("authorization_code") || - newClient.getGrantTypes().contains("implicit")) { - // return an error, you can't have these grant types together - m.addAttribute("error", "invalid_client_metadata"); - m.addAttribute("errorMessage", "Incompatible grant types requested: " + newClient.getGrantTypes()); - m.addAttribute("code", HttpStatus.BAD_REQUEST); - return "jsonErrorView"; - } - - if (!newClient.getResponseTypes().isEmpty()) { - // return an error, you can't have this grant type and response type together - m.addAttribute("error", "invalid_client_metadata"); - m.addAttribute("errorMessage", "Incompatible response types requested: " + newClient.getGrantTypes() + " / " + newClient.getResponseTypes()); - m.addAttribute("code", HttpStatus.BAD_REQUEST); - return "jsonErrorView"; - } - - // don't allow refresh tokens or id tokens in client_credentials clients - newClient.getGrantTypes().remove("refresh_token"); - newClient.getScope().remove("offline_access"); - newClient.getScope().remove("openid"); - } - if (newClient.getGrantTypes().isEmpty()) { - // return an error, you need at least one grant type selected - m.addAttribute("error", "invalid_client_metadata"); - m.addAttribute("errorMessage", "Clients must register at least one grant type."); - m.addAttribute("code", HttpStatus.BAD_REQUEST); - return "jsonErrorView"; - } - - // check to make sure this client registered a redirect URI if using a redirect flow - if (newClient.getGrantTypes().contains("authorization_code") || newClient.getGrantTypes().contains("implicit")) { - if (newClient.getRedirectUris() == null || newClient.getRedirectUris().isEmpty()) { - // return an error - m.addAttribute("error", "invalid_client_uri"); - m.addAttribute("errorMessage", "Clients using a redirect-based grant type must register at least one redirect URI."); - m.addAttribute("code", HttpStatus.BAD_REQUEST); - return "jsonErrorView"; - } - - for (String uri : newClient.getRedirectUris()) { - if (blacklistService.isBlacklisted(uri)) { - // return an error - m.addAttribute("error", "invalid_client_uri"); - m.addAttribute("errorMessage", "Redirect URI is not allowed: " + uri); - m.addAttribute("code", HttpStatus.BAD_REQUEST); - return "jsonErrorView"; - } - } - } - - if (newClient.getTokenEndpointAuthMethod() == null) { newClient.setTokenEndpointAuthMethod(AuthMethod.SECRET_BASIC); } @@ -420,147 +272,21 @@ public class ClientDynamicRegistrationEndpoint { 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); - - // if the client didn't ask for any, give them the defaults - if (allowedScopes == null || allowedScopes.isEmpty()) { - allowedScopes = scopeService.getDefaults(); - } - - // make sure that the client doesn't ask for scopes it can't have - newClient.setScope(scopeService.toStrings(allowedScopes)); - - if (newClient.getResponseTypes() == null) { - newClient.setResponseTypes(new HashSet()); - } - - // filter out unknown grant types - // TODO: make this a pluggable service - Set requestedGrantTypes = new HashSet(newClient.getGrantTypes()); - requestedGrantTypes.removeAll( - ImmutableSet.of("authorization_code", "implicit", - "password", "client_credentials", "refresh_token", - "urn:ietf:params:oauth:grant_type:redelegate")); - if (!requestedGrantTypes.isEmpty()) { - // return an error, there were unknown grant types requested - m.addAttribute("error", "invalid_client_metadata"); - m.addAttribute("errorMessage", "Unknown grant types requested: " + newClient.getGrantTypes()); - m.addAttribute("code", HttpStatus.BAD_REQUEST); - return "jsonErrorView"; - } - - // don't allow "password" grant type for dynamic registration - if (newClient.getGrantTypes().contains("password")) { - // return an error, you can't dynamically register for the password grant - m.addAttribute("error", "invalid_client_metadata"); - m.addAttribute("errorMessage", "The password grant type is not allowed in dynamic registration on this server."); - m.addAttribute("code", HttpStatus.BAD_REQUEST); + // do validation on the fields + try { + newClient = validateScopes(newClient); + newClient = validateResponseTypes(newClient); + newClient = validateGrantTypes(newClient); + newClient = validateRedirectUris(newClient); + newClient = validateAuth(newClient); + } catch (ValidationException ve) { + // validation failed, return an error + m.addAttribute("error", ve.getError()); + m.addAttribute("errorMessage", ve.getErrorDescription()); + m.addAttribute("code", ve.getStatus()); return "jsonErrorView"; } - - // don't allow clients to have multiple incompatible grant types and scopes - if (newClient.getGrantTypes().contains("authorization_code")) { - - // check for incompatible grants - if (newClient.getGrantTypes().contains("implicit") || - newClient.getGrantTypes().contains("client_credentials")) { - // return an error, you can't have these grant types together - m.addAttribute("error", "invalid_client_metadata"); - m.addAttribute("errorMessage", "Incompatible grant types requested: " + newClient.getGrantTypes()); - m.addAttribute("code", HttpStatus.BAD_REQUEST); - return "jsonErrorView"; - } - - newClient.getResponseTypes().add("code"); - - } - - if (newClient.getGrantTypes().contains("implicit")) { - - // check for incompatible grants - if (newClient.getGrantTypes().contains("authorization_code") || - newClient.getGrantTypes().contains("client_credentials")) { - // return an error, you can't have these grant types together - m.addAttribute("error", "invalid_client_metadata"); - m.addAttribute("errorMessage", "Incompatible grant types requested: " + newClient.getGrantTypes()); - m.addAttribute("code", HttpStatus.BAD_REQUEST); - return "jsonErrorView"; - } - - newClient.getResponseTypes().add("token"); - - // don't allow refresh tokens in implicit clients - newClient.getGrantTypes().remove("refresh_token"); - newClient.getScope().remove("offline_access"); - } - if (newClient.getGrantTypes().contains("client_credentials")) { - - // check for incompatible grants - if (newClient.getGrantTypes().contains("authorization_code") || - newClient.getGrantTypes().contains("implicit")) { - // return an error, you can't have these grant types together - m.addAttribute("error", "invalid_client_metadata"); - m.addAttribute("errorMessage", "Incompatible grant types requested: " + newClient.getGrantTypes()); - m.addAttribute("code", HttpStatus.BAD_REQUEST); - return "jsonErrorView"; - } - - // don't allow refresh tokens or id tokens in client_credentials clients - newClient.getGrantTypes().remove("refresh_token"); - newClient.getScope().remove("offline_access"); - newClient.getScope().remove("openid"); - } - - if (newClient.getGrantTypes().isEmpty()) { - // return an error, you need at least one grant type selected - m.addAttribute("error", "invalid_client_metadata"); - m.addAttribute("errorMessage", "Clients must register at least one grant type."); - m.addAttribute("code", HttpStatus.BAD_REQUEST); - return "jsonErrorView"; - } - - // check to make sure this client registered a redirect URI if using a redirect flow - if (newClient.getGrantTypes().contains("authorization_code") || newClient.getGrantTypes().contains("implicit")) { - if (newClient.getRedirectUris() == null || newClient.getRedirectUris().isEmpty()) { - // return an error - m.addAttribute("error", "invalid_client_uri"); - m.addAttribute("errorMessage", "Clients using a redirect-based grant type must register at least one redirect URI."); - m.addAttribute("code", HttpStatus.BAD_REQUEST); - return "jsonErrorView"; - } - - for (String uri : newClient.getRedirectUris()) { - if (blacklistService.isBlacklisted(uri)) { - // return an error - m.addAttribute("error", "invalid_client_uri"); - m.addAttribute("errorMessage", "Redirect URI is not allowed: " + uri); - m.addAttribute("code", HttpStatus.BAD_REQUEST); - return "jsonErrorView"; - } - } - } - - 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); - } - - try { // save the client ClientDetailsEntity savedClient = clientService.updateClient(oldClient, newClient); @@ -627,4 +353,198 @@ public class ClientDynamicRegistrationEndpoint { } } + private ClientDetailsEntity validateScopes(ClientDetailsEntity newClient) throws ValidationException { + // 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); + + // 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; + } + + private ClientDetailsEntity validateResponseTypes(ClientDetailsEntity newClient) throws ValidationException { + if (newClient.getResponseTypes() == null) { + newClient.setResponseTypes(new HashSet()); + } + return newClient; + } + + private ClientDetailsEntity validateGrantTypes(ClientDetailsEntity newClient) throws ValidationException { + // set default grant types if needed + if (newClient.getGrantTypes() == null || newClient.getGrantTypes().isEmpty()) { + if (newClient.getScope().contains("offline_access")) { // client asked for offline access + newClient.setGrantTypes(Sets.newHashSet("authorization_code", "refresh_token")); // allow authorization code and refresh token grant types by default + } else { + newClient.setGrantTypes(Sets.newHashSet("authorization_code")); // allow authorization code grant type by default + } + } + + // filter out unknown grant types + // TODO: make this a pluggable service + Set requestedGrantTypes = new HashSet(newClient.getGrantTypes()); + requestedGrantTypes.removeAll( + ImmutableSet.of("authorization_code", "implicit", + "password", "client_credentials", "refresh_token", + "urn:ietf:params:oauth:grant_type:redelegate")); + if (!requestedGrantTypes.isEmpty()) { + // return an error, there were unknown grant types requested + throw new ValidationException("invalid_client_metadata", "Unknown grant types requested: " + newClient.getGrantTypes(), HttpStatus.BAD_REQUEST); + } + + // don't allow "password" grant type for dynamic registration + if (newClient.getGrantTypes().contains("password")) { + // return an error, you can't dynamically register for the password grant + throw new ValidationException("invalid_client_metadata", "The password grant type is not allowed in dynamic registration on this server.", HttpStatus.BAD_REQUEST); + } + + // don't allow clients to have multiple incompatible grant types and scopes + if (newClient.getGrantTypes().contains("authorization_code")) { + + // check for incompatible grants + if (newClient.getGrantTypes().contains("implicit") || + newClient.getGrantTypes().contains("client_credentials")) { + // return an error, you can't have these grant types together + throw new ValidationException("invalid_client_metadata", "Incompatible grant types requested: " + newClient.getGrantTypes(), HttpStatus.BAD_REQUEST); + } + + if (newClient.getResponseTypes().contains("token")) { + // return an error, you can't have this grant type and response type together + throw new ValidationException("invalid_client_metadata", "Incompatible response types requested: " + newClient.getGrantTypes() + " / " + newClient.getResponseTypes(), HttpStatus.BAD_REQUEST); + } + + newClient.getResponseTypes().add("code"); + + + } + + if (newClient.getGrantTypes().contains("implicit")) { + + // check for incompatible grants + if (newClient.getGrantTypes().contains("authorization_code") || + newClient.getGrantTypes().contains("client_credentials")) { + // return an error, you can't have these grant types together + throw new ValidationException("invalid_client_metadata", "Incompatible grant types requested: " + newClient.getGrantTypes(), HttpStatus.BAD_REQUEST); + } + + if (newClient.getResponseTypes().contains("code")) { + // return an error, you can't have this grant type and response type together + throw new ValidationException("invalid_client_metadata", "Incompatible response types requested: " + newClient.getGrantTypes() + " / " + newClient.getResponseTypes(), HttpStatus.BAD_REQUEST); + } + + newClient.getResponseTypes().add("token"); + + // don't allow refresh tokens in implicit clients + newClient.getGrantTypes().remove("refresh_token"); + newClient.getScope().remove("offline_access"); + } + + if (newClient.getGrantTypes().contains("client_credentials")) { + + // check for incompatible grants + if (newClient.getGrantTypes().contains("authorization_code") || + newClient.getGrantTypes().contains("implicit")) { + // return an error, you can't have these grant types together + throw new ValidationException("invalid_client_metadata", "Incompatible grant types requested: " + newClient.getGrantTypes(), HttpStatus.BAD_REQUEST); + } + + if (!newClient.getResponseTypes().isEmpty()) { + // return an error, you can't have this grant type and response type together + throw new ValidationException("invalid_client_metadata", "Incompatible response types requested: " + newClient.getGrantTypes() + " / " + newClient.getResponseTypes(), HttpStatus.BAD_REQUEST); + } + + // don't allow refresh tokens or id tokens in client_credentials clients + newClient.getGrantTypes().remove("refresh_token"); + newClient.getScope().remove("offline_access"); + newClient.getScope().remove("openid"); + } + + if (newClient.getGrantTypes().isEmpty()) { + // return an error, you need at least one grant type selected + throw new ValidationException("invalid_client_metadata", "Clients must register at least one grant type.", HttpStatus.BAD_REQUEST); + } + return newClient; + } + + private ClientDetailsEntity validateRedirectUris(ClientDetailsEntity newClient) throws ValidationException { + // check to make sure this client registered a redirect URI if using a redirect flow + if (newClient.getGrantTypes().contains("authorization_code") || newClient.getGrantTypes().contains("implicit")) { + if (newClient.getRedirectUris() == null || newClient.getRedirectUris().isEmpty()) { + // return an error + throw new ValidationException("invalid_client_uri", "Clients using a redirect-based grant type must register at least one redirect URI.", HttpStatus.BAD_REQUEST); + } + + for (String uri : newClient.getRedirectUris()) { + if (blacklistService.isBlacklisted(uri)) { + // return an error + throw new ValidationException("invalid_client_uri", "Redirect URI is not allowed: " + uri, HttpStatus.BAD_REQUEST); + } + } + } + + return newClient; + } + + 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) { + + // we need to generate a secret + newClient = clientService.generateClientSecret(newClient); + } + return newClient; + } + + /** + * Thrown by utility methods when a client fails to validate. Contains information + * to be returned. + * @author jricher + * + */ + private class ValidationException extends Exception { + private String error; + private String errorDescription; + private HttpStatus status; + public ValidationException(String error, String errorDescription, + HttpStatus status) { + this.error = error; + this.errorDescription = errorDescription; + this.status = status; + } + public String getError() { + return error; + } + public void setError(String error) { + this.error = error; + } + public String getErrorDescription() { + return errorDescription; + } + public void setErrorDescription(String errorDescription) { + this.errorDescription = errorDescription; + } + public HttpStatus getStatus() { + return status; + } + public void setStatus(HttpStatus status) { + this.status = status; + } + + } + }