refactored dynamic registration endpoint's checks for client consistency

pull/612/head
Justin Richer 11 years ago
parent 22ab1173a7
commit deaccf437e

@ -112,168 +112,20 @@ public class ClientDynamicRegistrationEndpoint {
newClient.setClientId(null);
newClient.setClientSecret(null);
// set of scopes that are OK for clients to dynamically register for
Set<SystemScope> dynScopes = scopeService.getDynReg();
// scopes that the client is asking for
Set<SystemScope> requestedScopes = scopeService.fromStrings(newClient.getScope());
// the scopes that the client can have must be a subset of the dynamically allowed scopes
Set<SystemScope> 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<String>());
}
// filter out unknown grant types
// TODO: make this a pluggable service
Set<String> requestedGrantTypes = new HashSet<String>(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);
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);
// 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";
}
}
}
if (newClient.getTokenEndpointAuthMethod() == null) {
newClient.setTokenEndpointAuthMethod(AuthMethod.SECRET_BASIC);
@ -420,6 +272,88 @@ public class ClientDynamicRegistrationEndpoint {
newClient.setCreatedAt(oldClient.getCreatedAt());
newClient.setReuseRefreshToken(oldClient.isReuseRefreshToken());
// 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";
}
try {
// 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());
RegisteredClient registered = new RegisteredClient(savedClient, token.getValue(), config.getIssuer() + "register/" + UriUtils.encodePathSegment(savedClient.getClientId(), "UTF-8"));
// send it all out to the view
m.addAttribute("client", registered);
m.addAttribute("code", HttpStatus.OK); // http 200
return "clientInformationResponseView";
} catch (IllegalArgumentException e) {
logger.error("Couldn't save client", e);
m.addAttribute("code", HttpStatus.BAD_REQUEST);
return "httpCodeView";
} catch (UnsupportedEncodingException e) {
logger.error("Unsupported encoding", e);
m.addAttribute("code", HttpStatus.INTERNAL_SERVER_ERROR);
return "httpCodeView";
}
} else {
// client mismatch
logger.error("updateClient failed, client ID mismatch: "
+ clientId + " and " + auth.getOAuth2Request().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('" + SystemScopeService.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.getOAuth2Request().getClientId())) {
clientService.deleteClient(client);
m.addAttribute("code", HttpStatus.NO_CONTENT); // http 204
return "httpCodeView";
} else {
// client mismatch
logger.error("readClientConfiguration failed, client ID mismatch: "
+ clientId + " and " + auth.getOAuth2Request().getClientId() + " do not match.");
m.addAttribute("code", HttpStatus.FORBIDDEN); // http 403
return "httpCodeView";
}
}
private ClientDetailsEntity validateScopes(ClientDetailsEntity newClient) throws ValidationException {
// set of scopes that are OK for clients to dynamically register for
Set<SystemScope> dynScopes = scopeService.getDynReg();
@ -434,12 +368,27 @@ public class ClientDynamicRegistrationEndpoint {
allowedScopes = scopeService.getDefaults();
}
// make sure that the client doesn't ask for scopes it can't have
newClient.setScope(scopeService.toStrings(allowedScopes));
return newClient;
}
private ClientDetailsEntity validateResponseTypes(ClientDetailsEntity newClient) throws ValidationException {
if (newClient.getResponseTypes() == null) {
newClient.setResponseTypes(new HashSet<String>());
}
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
@ -450,19 +399,13 @@ public class ClientDynamicRegistrationEndpoint {
"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";
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
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);
return "jsonErrorView";
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
@ -472,14 +415,17 @@ public class ClientDynamicRegistrationEndpoint {
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";
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")) {
@ -488,10 +434,12 @@ public class ClientDynamicRegistrationEndpoint {
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";
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");
@ -507,10 +455,12 @@ public class ClientDynamicRegistrationEndpoint {
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";
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
@ -521,33 +471,31 @@ public class ClientDynamicRegistrationEndpoint {
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";
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
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";
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
m.addAttribute("error", "invalid_client_uri");
m.addAttribute("errorMessage", "Redirect URI is not allowed: " + uri);
m.addAttribute("code", HttpStatus.BAD_REQUEST);
return "jsonErrorView";
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);
}
@ -559,72 +507,44 @@ public class ClientDynamicRegistrationEndpoint {
// we need to generate a secret
newClient = clientService.generateClientSecret(newClient);
}
try {
// 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());
RegisteredClient registered = new RegisteredClient(savedClient, token.getValue(), config.getIssuer() + "register/" + UriUtils.encodePathSegment(savedClient.getClientId(), "UTF-8"));
// send it all out to the view
m.addAttribute("client", registered);
m.addAttribute("code", HttpStatus.OK); // http 200
return "clientInformationResponseView";
} catch (IllegalArgumentException e) {
logger.error("Couldn't save client", e);
m.addAttribute("code", HttpStatus.BAD_REQUEST);
return "httpCodeView";
} catch (UnsupportedEncodingException e) {
logger.error("Unsupported encoding", e);
m.addAttribute("code", HttpStatus.INTERNAL_SERVER_ERROR);
return "httpCodeView";
}
} else {
// client mismatch
logger.error("updateClient failed, client ID mismatch: "
+ clientId + " and " + auth.getOAuth2Request().getClientId() + " do not match.");
m.addAttribute("code", HttpStatus.FORBIDDEN); // http 403
return "httpCodeView";
}
return newClient;
}
/**
* Delete the indicated client from the system.
* @param clientId
* @param m
* @param auth
* @return
* Thrown by utility methods when a client fails to validate. Contains information
* to be returned.
* @author jricher
*
*/
@PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + SystemScopeService.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.getOAuth2Request().getClientId())) {
clientService.deleteClient(client);
m.addAttribute("code", HttpStatus.NO_CONTENT); // http 204
return "httpCodeView";
} else {
// client mismatch
logger.error("readClientConfiguration failed, client ID mismatch: "
+ clientId + " and " + auth.getOAuth2Request().getClientId() + " do not match.");
m.addAttribute("code", HttpStatus.FORBIDDEN); // http 403
return "httpCodeView";
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;
}
}
}

Loading…
Cancel
Save