001/******************************************************************************* 002 * Copyright 2017 The MIT Internet Trust Consortium 003 * 004 * Portions copyright 2011-2013 The MITRE Corporation 005 * 006 * Licensed under the Apache License, Version 2.0 (the "License"); 007 * you may not use this file except in compliance with the License. 008 * You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 *******************************************************************************/ 018package org.mitre.openid.connect.web; 019 020import java.io.UnsupportedEncodingException; 021import java.text.ParseException; 022import java.util.Date; 023import java.util.HashSet; 024import java.util.Set; 025import java.util.concurrent.TimeUnit; 026 027import org.mitre.jwt.assertion.AssertionValidator; 028import org.mitre.oauth2.model.ClientDetailsEntity; 029import org.mitre.oauth2.model.ClientDetailsEntity.AppType; 030import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod; 031import org.mitre.oauth2.model.ClientDetailsEntity.SubjectType; 032import org.mitre.oauth2.model.OAuth2AccessTokenEntity; 033import org.mitre.oauth2.model.RegisteredClient; 034import org.mitre.oauth2.model.SystemScope; 035import org.mitre.oauth2.service.ClientDetailsEntityService; 036import org.mitre.oauth2.service.OAuth2TokenEntityService; 037import org.mitre.oauth2.service.SystemScopeService; 038import org.mitre.openid.connect.ClientDetailsEntityJsonProcessor; 039import org.mitre.openid.connect.config.ConfigurationPropertiesBean; 040import org.mitre.openid.connect.exception.ValidationException; 041import org.mitre.openid.connect.service.BlacklistedSiteService; 042import org.mitre.openid.connect.service.OIDCTokenService; 043import org.mitre.openid.connect.view.ClientInformationResponseView; 044import org.mitre.openid.connect.view.HttpCodeView; 045import org.mitre.openid.connect.view.JsonErrorView; 046import org.slf4j.Logger; 047import org.slf4j.LoggerFactory; 048import org.springframework.beans.factory.annotation.Autowired; 049import org.springframework.beans.factory.annotation.Qualifier; 050import org.springframework.http.HttpStatus; 051import org.springframework.http.MediaType; 052import org.springframework.security.access.prepost.PreAuthorize; 053import org.springframework.security.oauth2.common.util.OAuth2Utils; 054import org.springframework.security.oauth2.provider.OAuth2Authentication; 055import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails; 056import org.springframework.stereotype.Controller; 057import org.springframework.ui.Model; 058import org.springframework.web.bind.annotation.PathVariable; 059import org.springframework.web.bind.annotation.RequestBody; 060import org.springframework.web.bind.annotation.RequestMapping; 061import org.springframework.web.bind.annotation.RequestMethod; 062import org.springframework.web.util.UriUtils; 063 064import com.google.common.base.Strings; 065import com.google.common.collect.ImmutableSet; 066import com.google.common.collect.Sets; 067import com.google.gson.JsonSyntaxException; 068import com.nimbusds.jose.EncryptionMethod; 069import com.nimbusds.jose.JWEAlgorithm; 070import com.nimbusds.jose.JWSAlgorithm; 071import com.nimbusds.jose.jwk.JWKSet; 072import com.nimbusds.jwt.JWTClaimsSet; 073 074import static org.mitre.oauth2.model.RegisteredClientFields.APPLICATION_TYPE; 075import static org.mitre.oauth2.model.RegisteredClientFields.CLAIMS_REDIRECT_URIS; 076import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_ID; 077import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_ID_ISSUED_AT; 078import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_NAME; 079import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_SECRET; 080import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_SECRET_EXPIRES_AT; 081import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_URI; 082import static org.mitre.oauth2.model.RegisteredClientFields.CONTACTS; 083import static org.mitre.oauth2.model.RegisteredClientFields.DEFAULT_ACR_VALUES; 084import static org.mitre.oauth2.model.RegisteredClientFields.DEFAULT_MAX_AGE; 085import static org.mitre.oauth2.model.RegisteredClientFields.GRANT_TYPES; 086import static org.mitre.oauth2.model.RegisteredClientFields.ID_TOKEN_ENCRYPTED_RESPONSE_ALG; 087import static org.mitre.oauth2.model.RegisteredClientFields.ID_TOKEN_ENCRYPTED_RESPONSE_ENC; 088import static org.mitre.oauth2.model.RegisteredClientFields.ID_TOKEN_SIGNED_RESPONSE_ALG; 089import static org.mitre.oauth2.model.RegisteredClientFields.INITIATE_LOGIN_URI; 090import static org.mitre.oauth2.model.RegisteredClientFields.JWKS; 091import static org.mitre.oauth2.model.RegisteredClientFields.JWKS_URI; 092import static org.mitre.oauth2.model.RegisteredClientFields.LOGO_URI; 093import static org.mitre.oauth2.model.RegisteredClientFields.POLICY_URI; 094import static org.mitre.oauth2.model.RegisteredClientFields.POST_LOGOUT_REDIRECT_URIS; 095import static org.mitre.oauth2.model.RegisteredClientFields.REDIRECT_URIS; 096import static org.mitre.oauth2.model.RegisteredClientFields.REGISTRATION_ACCESS_TOKEN; 097import static org.mitre.oauth2.model.RegisteredClientFields.REGISTRATION_CLIENT_URI; 098import static org.mitre.oauth2.model.RegisteredClientFields.REQUEST_OBJECT_SIGNING_ALG; 099import static org.mitre.oauth2.model.RegisteredClientFields.REQUEST_URIS; 100import static org.mitre.oauth2.model.RegisteredClientFields.REQUIRE_AUTH_TIME; 101import static org.mitre.oauth2.model.RegisteredClientFields.RESPONSE_TYPES; 102import static org.mitre.oauth2.model.RegisteredClientFields.SCOPE; 103import static org.mitre.oauth2.model.RegisteredClientFields.SECTOR_IDENTIFIER_URI; 104import static org.mitre.oauth2.model.RegisteredClientFields.SOFTWARE_STATEMENT; 105import static org.mitre.oauth2.model.RegisteredClientFields.SUBJECT_TYPE; 106import static org.mitre.oauth2.model.RegisteredClientFields.TOKEN_ENDPOINT_AUTH_METHOD; 107import static org.mitre.oauth2.model.RegisteredClientFields.TOKEN_ENDPOINT_AUTH_SIGNING_ALG; 108import static org.mitre.oauth2.model.RegisteredClientFields.TOS_URI; 109import static org.mitre.oauth2.model.RegisteredClientFields.USERINFO_ENCRYPTED_RESPONSE_ALG; 110import static org.mitre.oauth2.model.RegisteredClientFields.USERINFO_ENCRYPTED_RESPONSE_ENC; 111import static org.mitre.oauth2.model.RegisteredClientFields.USERINFO_SIGNED_RESPONSE_ALG; 112 113@Controller 114@RequestMapping(value = DynamicClientRegistrationEndpoint.URL) 115public class DynamicClientRegistrationEndpoint { 116 117 public static final String URL = "register"; 118 119 @Autowired 120 private ClientDetailsEntityService clientService; 121 122 @Autowired 123 private OAuth2TokenEntityService tokenService; 124 125 @Autowired 126 private SystemScopeService scopeService; 127 128 @Autowired 129 private BlacklistedSiteService blacklistService; 130 131 @Autowired 132 private ConfigurationPropertiesBean config; 133 134 @Autowired 135 private OIDCTokenService connectTokenService; 136 137 @Autowired 138 @Qualifier("clientAssertionValidator") 139 private AssertionValidator assertionValidator; 140 141 /** 142 * Logger for this class 143 */ 144 private static final Logger logger = LoggerFactory.getLogger(DynamicClientRegistrationEndpoint.class); 145 146 /** 147 * Create a new Client, issue a client ID, and create a registration access token. 148 * @param jsonString 149 * @param m 150 * @param p 151 * @return 152 */ 153 @RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) 154 public String registerNewClient(@RequestBody String jsonString, Model m) { 155 156 ClientDetailsEntity newClient = null; 157 try { 158 newClient = ClientDetailsEntityJsonProcessor.parse(jsonString); 159 } catch (JsonSyntaxException e) { 160 // bad parse 161 // didn't parse, this is a bad request 162 logger.error("registerNewClient failed; submitted JSON is malformed"); 163 m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400 164 return HttpCodeView.VIEWNAME; 165 } 166 167 if (newClient != null) { 168 // it parsed! 169 170 // 171 // Now do some post-processing consistency checks on it 172 // 173 174 // clear out any spurious id/secret (clients don't get to pick) 175 newClient.setClientId(null); 176 newClient.setClientSecret(null); 177 178 // do validation on the fields 179 try { 180 newClient = validateSoftwareStatement(newClient); // need to handle the software statement first because it might override requested values 181 newClient = validateScopes(newClient); 182 newClient = validateResponseTypes(newClient); 183 newClient = validateGrantTypes(newClient); 184 newClient = validateRedirectUris(newClient); 185 newClient = validateAuth(newClient); 186 } catch (ValidationException ve) { 187 // validation failed, return an error 188 m.addAttribute(JsonErrorView.ERROR, ve.getError()); 189 m.addAttribute(JsonErrorView.ERROR_MESSAGE, ve.getErrorDescription()); 190 m.addAttribute(HttpCodeView.CODE, ve.getStatus()); 191 return JsonErrorView.VIEWNAME; 192 } 193 194 if (newClient.getTokenEndpointAuthMethod() == null) { 195 newClient.setTokenEndpointAuthMethod(AuthMethod.SECRET_BASIC); 196 } 197 198 if (newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_BASIC || 199 newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_JWT || 200 newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_POST) { 201 202 // we need to generate a secret 203 newClient = clientService.generateClientSecret(newClient); 204 } 205 206 // set some defaults for token timeouts 207 if (config.isHeartMode()) { 208 // heart mode has different defaults depending on primary grant type 209 if (newClient.getGrantTypes().contains("authorization_code")) { 210 newClient.setAccessTokenValiditySeconds((int)TimeUnit.HOURS.toSeconds(1)); // access tokens good for 1hr 211 newClient.setIdTokenValiditySeconds((int)TimeUnit.MINUTES.toSeconds(5)); // id tokens good for 5min 212 newClient.setRefreshTokenValiditySeconds((int)TimeUnit.HOURS.toSeconds(24)); // refresh tokens good for 24hr 213 } else if (newClient.getGrantTypes().contains("implicit")) { 214 newClient.setAccessTokenValiditySeconds((int)TimeUnit.MINUTES.toSeconds(15)); // access tokens good for 15min 215 newClient.setIdTokenValiditySeconds((int)TimeUnit.MINUTES.toSeconds(5)); // id tokens good for 5min 216 newClient.setRefreshTokenValiditySeconds(0); // no refresh tokens 217 } else if (newClient.getGrantTypes().contains("client_credentials")) { 218 newClient.setAccessTokenValiditySeconds((int)TimeUnit.HOURS.toSeconds(6)); // access tokens good for 6hr 219 newClient.setIdTokenValiditySeconds(0); // no id tokens 220 newClient.setRefreshTokenValiditySeconds(0); // no refresh tokens 221 } 222 } else { 223 newClient.setAccessTokenValiditySeconds((int)TimeUnit.HOURS.toSeconds(1)); // access tokens good for 1hr 224 newClient.setIdTokenValiditySeconds((int)TimeUnit.MINUTES.toSeconds(10)); // id tokens good for 10min 225 newClient.setRefreshTokenValiditySeconds(null); // refresh tokens good until revoked 226 } 227 228 // this client has been dynamically registered (obviously) 229 newClient.setDynamicallyRegistered(true); 230 231 // this client can't do token introspection 232 newClient.setAllowIntrospection(false); 233 234 // now save it 235 try { 236 ClientDetailsEntity savedClient = clientService.saveNewClient(newClient); 237 238 // generate the registration access token 239 OAuth2AccessTokenEntity token = connectTokenService.createRegistrationAccessToken(savedClient); 240 token = tokenService.saveAccessToken(token); 241 242 // send it all out to the view 243 244 RegisteredClient registered = new RegisteredClient(savedClient, token.getValue(), config.getIssuer() + "register/" + UriUtils.encodePathSegment(savedClient.getClientId(), "UTF-8")); 245 m.addAttribute("client", registered); 246 m.addAttribute(HttpCodeView.CODE, HttpStatus.CREATED); // http 201 247 248 return ClientInformationResponseView.VIEWNAME; 249 } catch (UnsupportedEncodingException e) { 250 logger.error("Unsupported encoding", e); 251 m.addAttribute(HttpCodeView.CODE, HttpStatus.INTERNAL_SERVER_ERROR); 252 return HttpCodeView.VIEWNAME; 253 } catch (IllegalArgumentException e) { 254 logger.error("Couldn't save client", e); 255 256 m.addAttribute(JsonErrorView.ERROR, "invalid_client_metadata"); 257 m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unable to save client due to invalid or inconsistent metadata."); 258 m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400 259 260 return JsonErrorView.VIEWNAME; 261 } 262 } else { 263 // didn't parse, this is a bad request 264 logger.error("registerNewClient failed; submitted JSON is malformed"); 265 m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400 266 267 return HttpCodeView.VIEWNAME; 268 } 269 270 } 271 272 /** 273 * Get the meta information for a client. 274 * @param clientId 275 * @param m 276 * @param auth 277 * @return 278 */ 279 @PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + SystemScopeService.REGISTRATION_TOKEN_SCOPE + "')") 280 @RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) 281 public String readClientConfiguration(@PathVariable("id") String clientId, Model m, OAuth2Authentication auth) { 282 283 ClientDetailsEntity client = clientService.loadClientByClientId(clientId); 284 285 if (client != null && client.getClientId().equals(auth.getOAuth2Request().getClientId())) { 286 287 try { 288 OAuth2AccessTokenEntity token = rotateRegistrationTokenIfNecessary(auth, client); 289 RegisteredClient registered = new RegisteredClient(client, token.getValue(), config.getIssuer() + "register/" + UriUtils.encodePathSegment(client.getClientId(), "UTF-8")); 290 291 // send it all out to the view 292 m.addAttribute("client", registered); 293 m.addAttribute(HttpCodeView.CODE, HttpStatus.OK); // http 200 294 295 return ClientInformationResponseView.VIEWNAME; 296 } catch (UnsupportedEncodingException e) { 297 logger.error("Unsupported encoding", e); 298 m.addAttribute(HttpCodeView.CODE, HttpStatus.INTERNAL_SERVER_ERROR); 299 return HttpCodeView.VIEWNAME; 300 } 301 302 } else { 303 // client mismatch 304 logger.error("readClientConfiguration failed, client ID mismatch: " 305 + clientId + " and " + auth.getOAuth2Request().getClientId() + " do not match."); 306 m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); // http 403 307 308 return HttpCodeView.VIEWNAME; 309 } 310 } 311 312 /** 313 * Update the metainformation for a given client. 314 * @param clientId 315 * @param jsonString 316 * @param m 317 * @param auth 318 * @return 319 */ 320 @PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + SystemScopeService.REGISTRATION_TOKEN_SCOPE + "')") 321 @RequestMapping(value = "/{id}", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) 322 public String updateClient(@PathVariable("id") String clientId, @RequestBody String jsonString, Model m, OAuth2Authentication auth) { 323 324 325 ClientDetailsEntity newClient = null; 326 try { 327 newClient = ClientDetailsEntityJsonProcessor.parse(jsonString); 328 } catch (JsonSyntaxException e) { 329 // bad parse 330 // didn't parse, this is a bad request 331 logger.error("updateClient failed; submitted JSON is malformed"); 332 m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400 333 return HttpCodeView.VIEWNAME; 334 } 335 ClientDetailsEntity oldClient = clientService.loadClientByClientId(clientId); 336 337 if (newClient != null && oldClient != null // we have an existing client and the new one parsed 338 && oldClient.getClientId().equals(auth.getOAuth2Request().getClientId()) // the client passed in the URI matches the one in the auth 339 && oldClient.getClientId().equals(newClient.getClientId()) // the client passed in the body matches the one in the URI 340 ) { 341 342 // a client can't ask to update its own client secret to any particular value 343 newClient.setClientSecret(oldClient.getClientSecret()); 344 345 // we need to copy over all of the local and SECOAUTH fields 346 newClient.setAccessTokenValiditySeconds(oldClient.getAccessTokenValiditySeconds()); 347 newClient.setIdTokenValiditySeconds(oldClient.getIdTokenValiditySeconds()); 348 newClient.setRefreshTokenValiditySeconds(oldClient.getRefreshTokenValiditySeconds()); 349 newClient.setDynamicallyRegistered(true); // it's still dynamically registered 350 newClient.setAllowIntrospection(false); // dynamically registered clients can't do introspection -- use the resource registration instead 351 newClient.setAuthorities(oldClient.getAuthorities()); 352 newClient.setClientDescription(oldClient.getClientDescription()); 353 newClient.setCreatedAt(oldClient.getCreatedAt()); 354 newClient.setReuseRefreshToken(oldClient.isReuseRefreshToken()); 355 356 // do validation on the fields 357 try { 358 newClient = validateSoftwareStatement(newClient); // need to handle the software statement first because it might override requested values 359 newClient = validateScopes(newClient); 360 newClient = validateResponseTypes(newClient); 361 newClient = validateGrantTypes(newClient); 362 newClient = validateRedirectUris(newClient); 363 newClient = validateAuth(newClient); 364 } catch (ValidationException ve) { 365 // validation failed, return an error 366 m.addAttribute(JsonErrorView.ERROR, ve.getError()); 367 m.addAttribute(JsonErrorView.ERROR_MESSAGE, ve.getErrorDescription()); 368 m.addAttribute(HttpCodeView.CODE, ve.getStatus()); 369 return JsonErrorView.VIEWNAME; 370 } 371 372 try { 373 // save the client 374 ClientDetailsEntity savedClient = clientService.updateClient(oldClient, newClient); 375 376 OAuth2AccessTokenEntity token = rotateRegistrationTokenIfNecessary(auth, savedClient); 377 378 RegisteredClient registered = new RegisteredClient(savedClient, token.getValue(), config.getIssuer() + "register/" + UriUtils.encodePathSegment(savedClient.getClientId(), "UTF-8")); 379 380 // send it all out to the view 381 m.addAttribute("client", registered); 382 m.addAttribute(HttpCodeView.CODE, HttpStatus.OK); // http 200 383 384 return ClientInformationResponseView.VIEWNAME; 385 } catch (UnsupportedEncodingException e) { 386 logger.error("Unsupported encoding", e); 387 m.addAttribute(HttpCodeView.CODE, HttpStatus.INTERNAL_SERVER_ERROR); 388 return HttpCodeView.VIEWNAME; 389 } catch (IllegalArgumentException e) { 390 logger.error("Couldn't save client", e); 391 392 m.addAttribute(JsonErrorView.ERROR, "invalid_client_metadata"); 393 m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unable to save client due to invalid or inconsistent metadata."); 394 m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400 395 396 return JsonErrorView.VIEWNAME; 397 } 398 } else { 399 // client mismatch 400 logger.error("updateClient failed, client ID mismatch: " 401 + clientId + " and " + auth.getOAuth2Request().getClientId() + " do not match."); 402 m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); // http 403 403 404 return HttpCodeView.VIEWNAME; 405 } 406 } 407 408 /** 409 * Delete the indicated client from the system. 410 * @param clientId 411 * @param m 412 * @param auth 413 * @return 414 */ 415 @PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + SystemScopeService.REGISTRATION_TOKEN_SCOPE + "')") 416 @RequestMapping(value = "/{id}", method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_VALUE) 417 public String deleteClient(@PathVariable("id") String clientId, Model m, OAuth2Authentication auth) { 418 419 ClientDetailsEntity client = clientService.loadClientByClientId(clientId); 420 421 if (client != null && client.getClientId().equals(auth.getOAuth2Request().getClientId())) { 422 423 clientService.deleteClient(client); 424 425 m.addAttribute(HttpCodeView.CODE, HttpStatus.NO_CONTENT); // http 204 426 427 return HttpCodeView.VIEWNAME; 428 } else { 429 // client mismatch 430 logger.error("readClientConfiguration failed, client ID mismatch: " 431 + clientId + " and " + auth.getOAuth2Request().getClientId() + " do not match."); 432 m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); // http 403 433 434 return HttpCodeView.VIEWNAME; 435 } 436 } 437 438 private ClientDetailsEntity validateScopes(ClientDetailsEntity newClient) throws ValidationException { 439 // scopes that the client is asking for 440 Set<SystemScope> requestedScopes = scopeService.fromStrings(newClient.getScope()); 441 442 // the scopes that the client can have must be a subset of the dynamically allowed scopes 443 Set<SystemScope> allowedScopes = scopeService.removeRestrictedAndReservedScopes(requestedScopes); 444 445 // if the client didn't ask for any, give them the defaults 446 if (allowedScopes == null || allowedScopes.isEmpty()) { 447 allowedScopes = scopeService.getDefaults(); 448 } 449 450 newClient.setScope(scopeService.toStrings(allowedScopes)); 451 452 return newClient; 453 } 454 455 private ClientDetailsEntity validateResponseTypes(ClientDetailsEntity newClient) throws ValidationException { 456 if (newClient.getResponseTypes() == null) { 457 newClient.setResponseTypes(new HashSet<String>()); 458 } 459 return newClient; 460 } 461 462 private ClientDetailsEntity validateGrantTypes(ClientDetailsEntity newClient) throws ValidationException { 463 // set default grant types if needed 464 if (newClient.getGrantTypes() == null || newClient.getGrantTypes().isEmpty()) { 465 if (newClient.getScope().contains("offline_access")) { // client asked for offline access 466 newClient.setGrantTypes(Sets.newHashSet("authorization_code", "refresh_token")); // allow authorization code and refresh token grant types by default 467 } else { 468 newClient.setGrantTypes(Sets.newHashSet("authorization_code")); // allow authorization code grant type by default 469 } 470 if (config.isDualClient()) { 471 Set<String> extendedGrandTypes = newClient.getGrantTypes(); 472 extendedGrandTypes.add("client_credentials"); 473 newClient.setGrantTypes(extendedGrandTypes); 474 } 475 } 476 477 // filter out unknown grant types 478 // TODO: make this a pluggable service 479 Set<String> requestedGrantTypes = new HashSet<>(newClient.getGrantTypes()); 480 requestedGrantTypes.retainAll( 481 ImmutableSet.of("authorization_code", "implicit", 482 "password", "client_credentials", "refresh_token", 483 "urn:ietf:params:oauth:grant_type:redelegate")); 484 485 // don't allow "password" grant type for dynamic registration 486 if (newClient.getGrantTypes().contains("password")) { 487 // return an error, you can't dynamically register for the password grant 488 throw new ValidationException("invalid_client_metadata", "The password grant type is not allowed in dynamic registration on this server.", HttpStatus.BAD_REQUEST); 489 } 490 491 // don't allow clients to have multiple incompatible grant types and scopes 492 if (newClient.getGrantTypes().contains("authorization_code")) { 493 494 // check for incompatible grants 495 if (newClient.getGrantTypes().contains("implicit") || 496 (!config.isDualClient() && newClient.getGrantTypes().contains("client_credentials"))) { 497 // return an error, you can't have these grant types together 498 throw new ValidationException("invalid_client_metadata", "Incompatible grant types requested: " + newClient.getGrantTypes(), HttpStatus.BAD_REQUEST); 499 } 500 501 if (newClient.getResponseTypes().contains("token")) { 502 // return an error, you can't have this grant type and response type together 503 throw new ValidationException("invalid_client_metadata", "Incompatible response types requested: " + newClient.getGrantTypes() + " / " + newClient.getResponseTypes(), HttpStatus.BAD_REQUEST); 504 } 505 506 newClient.getResponseTypes().add("code"); 507 } 508 509 if (newClient.getGrantTypes().contains("implicit")) { 510 511 // check for incompatible grants 512 if (newClient.getGrantTypes().contains("authorization_code") || 513 (!config.isDualClient() && newClient.getGrantTypes().contains("client_credentials"))) { 514 // return an error, you can't have these grant types together 515 throw new ValidationException("invalid_client_metadata", "Incompatible grant types requested: " + newClient.getGrantTypes(), HttpStatus.BAD_REQUEST); 516 } 517 518 if (newClient.getResponseTypes().contains("code")) { 519 // return an error, you can't have this grant type and response type together 520 throw new ValidationException("invalid_client_metadata", "Incompatible response types requested: " + newClient.getGrantTypes() + " / " + newClient.getResponseTypes(), HttpStatus.BAD_REQUEST); 521 } 522 523 newClient.getResponseTypes().add("token"); 524 525 // don't allow refresh tokens in implicit clients 526 newClient.getGrantTypes().remove("refresh_token"); 527 newClient.getScope().remove(SystemScopeService.OFFLINE_ACCESS); 528 } 529 530 if (newClient.getGrantTypes().contains("client_credentials")) { 531 532 // check for incompatible grants 533 if (!config.isDualClient() && 534 (newClient.getGrantTypes().contains("authorization_code") || newClient.getGrantTypes().contains("implicit"))) { 535 // return an error, you can't have these grant types together 536 throw new ValidationException("invalid_client_metadata", "Incompatible grant types requested: " + newClient.getGrantTypes(), HttpStatus.BAD_REQUEST); 537 } 538 539 if (!newClient.getResponseTypes().isEmpty()) { 540 // return an error, you can't have this grant type and response type together 541 throw new ValidationException("invalid_client_metadata", "Incompatible response types requested: " + newClient.getGrantTypes() + " / " + newClient.getResponseTypes(), HttpStatus.BAD_REQUEST); 542 } 543 544 // don't allow refresh tokens or id tokens in client_credentials clients 545 newClient.getGrantTypes().remove("refresh_token"); 546 newClient.getScope().remove(SystemScopeService.OFFLINE_ACCESS); 547 newClient.getScope().remove(SystemScopeService.OPENID_SCOPE); 548 } 549 550 if (newClient.getGrantTypes().isEmpty()) { 551 // return an error, you need at least one grant type selected 552 throw new ValidationException("invalid_client_metadata", "Clients must register at least one grant type.", HttpStatus.BAD_REQUEST); 553 } 554 return newClient; 555 } 556 557 private ClientDetailsEntity validateRedirectUris(ClientDetailsEntity newClient) throws ValidationException { 558 // check to make sure this client registered a redirect URI if using a redirect flow 559 if (newClient.getGrantTypes().contains("authorization_code") || newClient.getGrantTypes().contains("implicit")) { 560 if (newClient.getRedirectUris() == null || newClient.getRedirectUris().isEmpty()) { 561 // return an error 562 throw new ValidationException("invalid_redirect_uri", "Clients using a redirect-based grant type must register at least one redirect URI.", HttpStatus.BAD_REQUEST); 563 } 564 565 for (String uri : newClient.getRedirectUris()) { 566 if (blacklistService.isBlacklisted(uri)) { 567 // return an error 568 throw new ValidationException("invalid_redirect_uri", "Redirect URI is not allowed: " + uri, HttpStatus.BAD_REQUEST); 569 } 570 571 if (uri.contains("#")) { 572 // if it contains the hash symbol then it has a fragment, which isn't allowed 573 throw new ValidationException("invalid_redirect_uri", "Redirect URI can not have a fragment", HttpStatus.BAD_REQUEST); 574 } 575 } 576 } 577 578 return newClient; 579 } 580 581 private ClientDetailsEntity validateAuth(ClientDetailsEntity newClient) throws ValidationException { 582 if (newClient.getTokenEndpointAuthMethod() == null) { 583 newClient.setTokenEndpointAuthMethod(AuthMethod.SECRET_BASIC); 584 } 585 586 if (newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_BASIC || 587 newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_JWT || 588 newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_POST) { 589 590 if (Strings.isNullOrEmpty(newClient.getClientSecret())) { 591 // no secret yet, we need to generate a secret 592 newClient = clientService.generateClientSecret(newClient); 593 } 594 } else if (newClient.getTokenEndpointAuthMethod() == AuthMethod.PRIVATE_KEY) { 595 if (Strings.isNullOrEmpty(newClient.getJwksUri()) && newClient.getJwks() == null) { 596 throw new ValidationException("invalid_client_metadata", "JWK Set URI required when using private key authentication", HttpStatus.BAD_REQUEST); 597 } 598 599 newClient.setClientSecret(null); 600 } else if (newClient.getTokenEndpointAuthMethod() == AuthMethod.NONE) { 601 newClient.setClientSecret(null); 602 } else { 603 throw new ValidationException("invalid_client_metadata", "Unknown authentication method", HttpStatus.BAD_REQUEST); 604 } 605 return newClient; 606 } 607 608 609 /** 610 * @param newClient 611 * @return 612 * @throws ValidationException 613 */ 614 private ClientDetailsEntity validateSoftwareStatement(ClientDetailsEntity newClient) throws ValidationException { 615 if (newClient.getSoftwareStatement() != null) { 616 if (assertionValidator.isValid(newClient.getSoftwareStatement())) { 617 // we have a software statement and its envelope passed all the checks from our validator 618 619 // swap out all of the client's fields for the associated parts of the software statement 620 try { 621 JWTClaimsSet claimSet = newClient.getSoftwareStatement().getJWTClaimsSet(); 622 for (String claim : claimSet.getClaims().keySet()) { 623 switch (claim) { 624 case SOFTWARE_STATEMENT: 625 throw new ValidationException("invalid_client_metadata", "Software statement can't include another software statement", HttpStatus.BAD_REQUEST); 626 case CLAIMS_REDIRECT_URIS: 627 newClient.setClaimsRedirectUris(Sets.newHashSet(claimSet.getStringListClaim(claim))); 628 break; 629 case CLIENT_SECRET_EXPIRES_AT: 630 throw new ValidationException("invalid_client_metadata", "Software statement can't include a client secret expiration time", HttpStatus.BAD_REQUEST); 631 case CLIENT_ID_ISSUED_AT: 632 throw new ValidationException("invalid_client_metadata", "Software statement can't include a client ID issuance time", HttpStatus.BAD_REQUEST); 633 case REGISTRATION_CLIENT_URI: 634 throw new ValidationException("invalid_client_metadata", "Software statement can't include a client configuration endpoint", HttpStatus.BAD_REQUEST); 635 case REGISTRATION_ACCESS_TOKEN: 636 throw new ValidationException("invalid_client_metadata", "Software statement can't include a client registration access token", HttpStatus.BAD_REQUEST); 637 case REQUEST_URIS: 638 newClient.setRequestUris(Sets.newHashSet(claimSet.getStringListClaim(claim))); 639 break; 640 case POST_LOGOUT_REDIRECT_URIS: 641 newClient.setPostLogoutRedirectUris(Sets.newHashSet(claimSet.getStringListClaim(claim))); 642 break; 643 case INITIATE_LOGIN_URI: 644 newClient.setInitiateLoginUri(claimSet.getStringClaim(claim)); 645 break; 646 case DEFAULT_ACR_VALUES: 647 newClient.setDefaultACRvalues(Sets.newHashSet(claimSet.getStringListClaim(claim))); 648 break; 649 case REQUIRE_AUTH_TIME: 650 newClient.setRequireAuthTime(claimSet.getBooleanClaim(claim)); 651 break; 652 case DEFAULT_MAX_AGE: 653 newClient.setDefaultMaxAge(claimSet.getIntegerClaim(claim)); 654 break; 655 case TOKEN_ENDPOINT_AUTH_SIGNING_ALG: 656 newClient.setTokenEndpointAuthSigningAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim))); 657 break; 658 case ID_TOKEN_ENCRYPTED_RESPONSE_ENC: 659 newClient.setIdTokenEncryptedResponseEnc(EncryptionMethod.parse(claimSet.getStringClaim(claim))); 660 break; 661 case ID_TOKEN_ENCRYPTED_RESPONSE_ALG: 662 newClient.setIdTokenEncryptedResponseAlg(JWEAlgorithm.parse(claimSet.getStringClaim(claim))); 663 break; 664 case ID_TOKEN_SIGNED_RESPONSE_ALG: 665 newClient.setIdTokenSignedResponseAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim))); 666 break; 667 case USERINFO_ENCRYPTED_RESPONSE_ENC: 668 newClient.setUserInfoEncryptedResponseEnc(EncryptionMethod.parse(claimSet.getStringClaim(claim))); 669 break; 670 case USERINFO_ENCRYPTED_RESPONSE_ALG: 671 newClient.setUserInfoEncryptedResponseAlg(JWEAlgorithm.parse(claimSet.getStringClaim(claim))); 672 break; 673 case USERINFO_SIGNED_RESPONSE_ALG: 674 newClient.setUserInfoSignedResponseAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim))); 675 break; 676 case REQUEST_OBJECT_SIGNING_ALG: 677 newClient.setRequestObjectSigningAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim))); 678 break; 679 case SUBJECT_TYPE: 680 newClient.setSubjectType(SubjectType.getByValue(claimSet.getStringClaim(claim))); 681 break; 682 case SECTOR_IDENTIFIER_URI: 683 newClient.setSectorIdentifierUri(claimSet.getStringClaim(claim)); 684 break; 685 case APPLICATION_TYPE: 686 newClient.setApplicationType(AppType.getByValue(claimSet.getStringClaim(claim))); 687 break; 688 case JWKS_URI: 689 newClient.setJwksUri(claimSet.getStringClaim(claim)); 690 break; 691 case JWKS: 692 newClient.setJwks(JWKSet.parse(claimSet.getJSONObjectClaim(claim).toJSONString())); 693 break; 694 case POLICY_URI: 695 newClient.setPolicyUri(claimSet.getStringClaim(claim)); 696 break; 697 case RESPONSE_TYPES: 698 newClient.setResponseTypes(Sets.newHashSet(claimSet.getStringListClaim(claim))); 699 break; 700 case GRANT_TYPES: 701 newClient.setGrantTypes(Sets.newHashSet(claimSet.getStringListClaim(claim))); 702 break; 703 case SCOPE: 704 newClient.setScope(OAuth2Utils.parseParameterList(claimSet.getStringClaim(claim))); 705 break; 706 case TOKEN_ENDPOINT_AUTH_METHOD: 707 newClient.setTokenEndpointAuthMethod(AuthMethod.getByValue(claimSet.getStringClaim(claim))); 708 break; 709 case TOS_URI: 710 newClient.setTosUri(claimSet.getStringClaim(claim)); 711 break; 712 case CONTACTS: 713 newClient.setContacts(Sets.newHashSet(claimSet.getStringListClaim(claim))); 714 break; 715 case LOGO_URI: 716 newClient.setLogoUri(claimSet.getStringClaim(claim)); 717 break; 718 case CLIENT_URI: 719 newClient.setClientUri(claimSet.getStringClaim(claim)); 720 break; 721 case CLIENT_NAME: 722 newClient.setClientName(claimSet.getStringClaim(claim)); 723 break; 724 case REDIRECT_URIS: 725 newClient.setRedirectUris(Sets.newHashSet(claimSet.getStringListClaim(claim))); 726 break; 727 case CLIENT_SECRET: 728 throw new ValidationException("invalid_client_metadata", "Software statement can't contain client secret", HttpStatus.BAD_REQUEST); 729 case CLIENT_ID: 730 throw new ValidationException("invalid_client_metadata", "Software statement can't contain client ID", HttpStatus.BAD_REQUEST); 731 732 default: 733 logger.warn("Software statement contained unknown field: " + claim + " with value " + claimSet.getClaim(claim)); 734 break; 735 } 736 } 737 738 return newClient; 739 } catch (ParseException e) { 740 throw new ValidationException("invalid_client_metadata", "Software statement claims didn't parse", HttpStatus.BAD_REQUEST); 741 } 742 } else { 743 throw new ValidationException("invalid_client_metadata", "Software statement rejected by validator", HttpStatus.BAD_REQUEST); 744 } 745 } else { 746 // nothing to see here, carry on 747 return newClient; 748 } 749 750 } 751 752 753 /* 754 * Rotates the registration token if it's expired, otherwise returns it 755 */ 756 private OAuth2AccessTokenEntity rotateRegistrationTokenIfNecessary(OAuth2Authentication auth, ClientDetailsEntity client) { 757 758 OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) auth.getDetails(); 759 OAuth2AccessTokenEntity token = tokenService.readAccessToken(details.getTokenValue()); 760 761 if (config.getRegTokenLifeTime() != null) { 762 763 try { 764 // Re-issue the token if it has been issued before [currentTime - validity] 765 Date validToDate = new Date(System.currentTimeMillis() - config.getRegTokenLifeTime() * 1000); 766 if(token.getJwt().getJWTClaimsSet().getIssueTime().before(validToDate)) { 767 logger.info("Rotating the registration access token for " + client.getClientId()); 768 tokenService.revokeAccessToken(token); 769 OAuth2AccessTokenEntity newToken = connectTokenService.createRegistrationAccessToken(client); 770 tokenService.saveAccessToken(newToken); 771 return newToken; 772 } else { 773 // it's not expired, keep going 774 return token; 775 } 776 } catch (ParseException e) { 777 logger.error("Couldn't parse a known-valid token?", e); 778 return token; 779 } 780 } else { 781 // tokens don't expire, just return it 782 return token; 783 } 784 } 785 786}