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.lang.reflect.Type; 021import java.sql.SQLIntegrityConstraintViolationException; 022import java.text.ParseException; 023import java.util.Collection; 024 025import javax.persistence.PersistenceException; 026 027import org.eclipse.persistence.exceptions.DatabaseException; 028import org.mitre.jwt.assertion.AssertionValidator; 029import org.mitre.oauth2.model.ClientDetailsEntity; 030import org.mitre.oauth2.model.ClientDetailsEntity.AppType; 031import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod; 032import org.mitre.oauth2.model.ClientDetailsEntity.SubjectType; 033import org.mitre.oauth2.model.PKCEAlgorithm; 034import org.mitre.oauth2.service.ClientDetailsEntityService; 035import org.mitre.oauth2.web.AuthenticationUtilities; 036import org.mitre.openid.connect.exception.ValidationException; 037import org.mitre.openid.connect.model.CachedImage; 038import org.mitre.openid.connect.service.ClientLogoLoadingService; 039import org.mitre.openid.connect.view.ClientEntityViewForAdmins; 040import org.mitre.openid.connect.view.ClientEntityViewForUsers; 041import org.mitre.openid.connect.view.HttpCodeView; 042import org.mitre.openid.connect.view.JsonEntityView; 043import org.mitre.openid.connect.view.JsonErrorView; 044import org.slf4j.Logger; 045import org.slf4j.LoggerFactory; 046import org.springframework.beans.factory.annotation.Autowired; 047import org.springframework.beans.factory.annotation.Qualifier; 048import org.springframework.http.HttpHeaders; 049import org.springframework.http.HttpStatus; 050import org.springframework.http.MediaType; 051import org.springframework.http.ResponseEntity; 052import org.springframework.security.access.prepost.PreAuthorize; 053import org.springframework.security.core.Authentication; 054import org.springframework.security.oauth2.common.util.OAuth2Utils; 055import org.springframework.stereotype.Controller; 056import org.springframework.ui.Model; 057import org.springframework.web.bind.annotation.PathVariable; 058import org.springframework.web.bind.annotation.RequestBody; 059import org.springframework.web.bind.annotation.RequestMapping; 060import org.springframework.web.bind.annotation.RequestMethod; 061import org.springframework.web.servlet.ModelAndView; 062 063import com.google.common.base.Strings; 064import com.google.common.collect.Sets; 065import com.google.gson.Gson; 066import com.google.gson.GsonBuilder; 067import com.google.gson.JsonDeserializationContext; 068import com.google.gson.JsonDeserializer; 069import com.google.gson.JsonElement; 070import com.google.gson.JsonObject; 071import com.google.gson.JsonParseException; 072import com.google.gson.JsonParser; 073import com.google.gson.JsonSyntaxException; 074import com.nimbusds.jose.Algorithm; 075import com.nimbusds.jose.EncryptionMethod; 076import com.nimbusds.jose.JWEAlgorithm; 077import com.nimbusds.jose.JWSAlgorithm; 078import com.nimbusds.jose.jwk.JWKSet; 079import com.nimbusds.jwt.JWT; 080import com.nimbusds.jwt.JWTClaimsSet; 081import com.nimbusds.jwt.JWTParser; 082 083import static org.mitre.oauth2.model.RegisteredClientFields.APPLICATION_TYPE; 084import static org.mitre.oauth2.model.RegisteredClientFields.CLAIMS_REDIRECT_URIS; 085import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_ID; 086import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_ID_ISSUED_AT; 087import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_NAME; 088import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_SECRET; 089import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_SECRET_EXPIRES_AT; 090import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_URI; 091import static org.mitre.oauth2.model.RegisteredClientFields.CONTACTS; 092import static org.mitre.oauth2.model.RegisteredClientFields.DEFAULT_ACR_VALUES; 093import static org.mitre.oauth2.model.RegisteredClientFields.DEFAULT_MAX_AGE; 094import static org.mitre.oauth2.model.RegisteredClientFields.GRANT_TYPES; 095import static org.mitre.oauth2.model.RegisteredClientFields.ID_TOKEN_ENCRYPTED_RESPONSE_ALG; 096import static org.mitre.oauth2.model.RegisteredClientFields.ID_TOKEN_ENCRYPTED_RESPONSE_ENC; 097import static org.mitre.oauth2.model.RegisteredClientFields.ID_TOKEN_SIGNED_RESPONSE_ALG; 098import static org.mitre.oauth2.model.RegisteredClientFields.INITIATE_LOGIN_URI; 099import static org.mitre.oauth2.model.RegisteredClientFields.JWKS; 100import static org.mitre.oauth2.model.RegisteredClientFields.JWKS_URI; 101import static org.mitre.oauth2.model.RegisteredClientFields.LOGO_URI; 102import static org.mitre.oauth2.model.RegisteredClientFields.POLICY_URI; 103import static org.mitre.oauth2.model.RegisteredClientFields.POST_LOGOUT_REDIRECT_URIS; 104import static org.mitre.oauth2.model.RegisteredClientFields.REDIRECT_URIS; 105import static org.mitre.oauth2.model.RegisteredClientFields.REGISTRATION_ACCESS_TOKEN; 106import static org.mitre.oauth2.model.RegisteredClientFields.REGISTRATION_CLIENT_URI; 107import static org.mitre.oauth2.model.RegisteredClientFields.REQUEST_OBJECT_SIGNING_ALG; 108import static org.mitre.oauth2.model.RegisteredClientFields.REQUEST_URIS; 109import static org.mitre.oauth2.model.RegisteredClientFields.REQUIRE_AUTH_TIME; 110import static org.mitre.oauth2.model.RegisteredClientFields.RESPONSE_TYPES; 111import static org.mitre.oauth2.model.RegisteredClientFields.SCOPE; 112import static org.mitre.oauth2.model.RegisteredClientFields.SECTOR_IDENTIFIER_URI; 113import static org.mitre.oauth2.model.RegisteredClientFields.SOFTWARE_STATEMENT; 114import static org.mitre.oauth2.model.RegisteredClientFields.SUBJECT_TYPE; 115import static org.mitre.oauth2.model.RegisteredClientFields.TOKEN_ENDPOINT_AUTH_METHOD; 116import static org.mitre.oauth2.model.RegisteredClientFields.TOKEN_ENDPOINT_AUTH_SIGNING_ALG; 117import static org.mitre.oauth2.model.RegisteredClientFields.TOS_URI; 118import static org.mitre.oauth2.model.RegisteredClientFields.USERINFO_ENCRYPTED_RESPONSE_ALG; 119import static org.mitre.oauth2.model.RegisteredClientFields.USERINFO_ENCRYPTED_RESPONSE_ENC; 120import static org.mitre.oauth2.model.RegisteredClientFields.USERINFO_SIGNED_RESPONSE_ALG; 121 122/** 123 * @author Michael Jett <mjett@mitre.org> 124 */ 125 126@Controller 127@RequestMapping("/" + ClientAPI.URL) 128@PreAuthorize("hasRole('ROLE_USER')") 129public class ClientAPI { 130 131 public static final String URL = RootController.API_URL + "/clients"; 132 133 @Autowired 134 private ClientDetailsEntityService clientService; 135 136 @Autowired 137 private ClientLogoLoadingService clientLogoLoadingService; 138 139 @Autowired 140 @Qualifier("clientAssertionValidator") 141 private AssertionValidator assertionValidator; 142 143 private JsonParser parser = new JsonParser(); 144 145 private Gson gson = new GsonBuilder() 146 .serializeNulls() 147 .registerTypeAdapter(JWSAlgorithm.class, new JsonDeserializer<Algorithm>() { 148 @Override 149 public JWSAlgorithm deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { 150 if (json.isJsonPrimitive()) { 151 return JWSAlgorithm.parse(json.getAsString()); 152 } else { 153 return null; 154 } 155 } 156 }) 157 .registerTypeAdapter(JWEAlgorithm.class, new JsonDeserializer<Algorithm>() { 158 @Override 159 public JWEAlgorithm deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { 160 if (json.isJsonPrimitive()) { 161 return JWEAlgorithm.parse(json.getAsString()); 162 } else { 163 return null; 164 } 165 } 166 }) 167 .registerTypeAdapter(EncryptionMethod.class, new JsonDeserializer<Algorithm>() { 168 @Override 169 public EncryptionMethod deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { 170 if (json.isJsonPrimitive()) { 171 return EncryptionMethod.parse(json.getAsString()); 172 } else { 173 return null; 174 } 175 } 176 }) 177 .registerTypeAdapter(JWKSet.class, new JsonDeserializer<JWKSet>() { 178 @Override 179 public JWKSet deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { 180 if (json.isJsonObject()) { 181 try { 182 return JWKSet.parse(json.toString()); 183 } catch (ParseException e) { 184 return null; 185 } 186 } else { 187 return null; 188 } 189 } 190 }) 191 .registerTypeAdapter(JWT.class, new JsonDeserializer<JWT>() { 192 @Override 193 public JWT deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { 194 if (json.isJsonPrimitive()) { 195 try { 196 return JWTParser.parse(json.getAsString()); 197 } catch (ParseException e) { 198 return null; 199 } 200 } else { 201 return null; 202 } 203 } 204 }) 205 .registerTypeAdapter(PKCEAlgorithm.class, new JsonDeserializer<Algorithm>() { 206 @Override 207 public PKCEAlgorithm deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { 208 if (json.isJsonPrimitive()) { 209 return PKCEAlgorithm.parse(json.getAsString()); 210 } else { 211 return null; 212 } 213 } 214 }) 215 .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") 216 .create(); 217 218 /** 219 * Logger for this class 220 */ 221 private static final Logger logger = LoggerFactory.getLogger(ClientAPI.class); 222 223 /** 224 * Get a list of all clients 225 * @param modelAndView 226 * @return 227 */ 228 @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) 229 public String apiGetAllClients(Model model, Authentication auth) { 230 231 Collection<ClientDetailsEntity> clients = clientService.getAllClients(); 232 model.addAttribute(JsonEntityView.ENTITY, clients); 233 234 if (AuthenticationUtilities.isAdmin(auth)) { 235 return ClientEntityViewForAdmins.VIEWNAME; 236 } else { 237 return ClientEntityViewForUsers.VIEWNAME; 238 } 239 } 240 241 /** 242 * Create a new client 243 * @param json 244 * @param m 245 * @param principal 246 * @return 247 */ 248 @PreAuthorize("hasRole('ROLE_ADMIN')") 249 @RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) 250 public String apiAddClient(@RequestBody String jsonString, Model m, Authentication auth) { 251 252 JsonObject json = null; 253 ClientDetailsEntity client = null; 254 255 try { 256 json = parser.parse(jsonString).getAsJsonObject(); 257 client = gson.fromJson(json, ClientDetailsEntity.class); 258 client = validateSoftwareStatement(client); 259 } catch (JsonSyntaxException e) { 260 logger.error("apiAddClient failed due to JsonSyntaxException", e); 261 m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); 262 m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Could not save new client. The server encountered a JSON syntax exception. Contact a system administrator for assistance."); 263 return JsonErrorView.VIEWNAME; 264 } catch (IllegalStateException e) { 265 logger.error("apiAddClient failed due to IllegalStateException", e); 266 m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); 267 m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Could not save new client. The server encountered an IllegalStateException. Refresh and try again - if the problem persists, contact a system administrator for assistance."); 268 return JsonErrorView.VIEWNAME; 269 } catch (ValidationException e) { 270 logger.error("apiUpdateClient failed due to ValidationException", e); 271 m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); 272 m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Could not update client. The server encountered a ValidationException."); 273 return JsonErrorView.VIEWNAME; 274 } 275 276 // if they leave the client identifier empty, force it to be generated 277 if (Strings.isNullOrEmpty(client.getClientId())) { 278 client = clientService.generateClientId(client); 279 } 280 281 if (client.getTokenEndpointAuthMethod() == null || 282 client.getTokenEndpointAuthMethod().equals(AuthMethod.NONE)) { 283 // we shouldn't have a secret for this client 284 285 client.setClientSecret(null); 286 287 } else if (client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_BASIC) 288 || client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_POST) 289 || client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_JWT)) { 290 291 // if they've asked for us to generate a client secret (or they left it blank but require one), do so here 292 if (json.has("generateClientSecret") && json.get("generateClientSecret").getAsBoolean() 293 || Strings.isNullOrEmpty(client.getClientSecret())) { 294 client = clientService.generateClientSecret(client); 295 } 296 297 } else if (client.getTokenEndpointAuthMethod().equals(AuthMethod.PRIVATE_KEY)) { 298 299 if (Strings.isNullOrEmpty(client.getJwksUri()) && client.getJwks() == null) { 300 logger.error("tried to create client with private key auth but no private key"); 301 m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); 302 m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Can not create a client with private key authentication without registering a key via the JWK Set URI or JWK Set Value."); 303 return JsonErrorView.VIEWNAME; 304 } 305 306 // otherwise we shouldn't have a secret for this client 307 client.setClientSecret(null); 308 309 } else { 310 311 logger.error("unknown auth method"); 312 m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); 313 m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unknown auth method requested"); 314 return JsonErrorView.VIEWNAME; 315 316 317 } 318 319 client.setDynamicallyRegistered(false); 320 321 try { 322 ClientDetailsEntity newClient = clientService.saveNewClient(client); 323 m.addAttribute(JsonEntityView.ENTITY, newClient); 324 325 if (AuthenticationUtilities.isAdmin(auth)) { 326 return ClientEntityViewForAdmins.VIEWNAME; 327 } else { 328 return ClientEntityViewForUsers.VIEWNAME; 329 } 330 } catch (IllegalArgumentException e) { 331 logger.error("Unable to save client: {}", e.getMessage()); 332 m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); 333 m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unable to save client: " + e.getMessage()); 334 return JsonErrorView.VIEWNAME; 335 } catch (PersistenceException e) { 336 Throwable cause = e.getCause(); 337 if (cause instanceof DatabaseException) { 338 Throwable databaseExceptionCause = cause.getCause(); 339 if(databaseExceptionCause instanceof SQLIntegrityConstraintViolationException) { 340 logger.error("apiAddClient failed; duplicate client id entry found: {}", client.getClientId()); 341 m.addAttribute(HttpCodeView.CODE, HttpStatus.CONFLICT); 342 m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unable to save client. Duplicate client id entry found: " + client.getClientId()); 343 return JsonErrorView.VIEWNAME; 344 } 345 } 346 throw e; 347 } 348 } 349 350 /** 351 * Update an existing client 352 * @param id 353 * @param jsonString 354 * @param m 355 * @param principal 356 * @return 357 */ 358 @PreAuthorize("hasRole('ROLE_ADMIN')") 359 @RequestMapping(value="/{id}", method = RequestMethod.PUT, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) 360 public String apiUpdateClient(@PathVariable("id") Long id, @RequestBody String jsonString, Model m, Authentication auth) { 361 362 JsonObject json = null; 363 ClientDetailsEntity client = null; 364 365 try { 366 // parse the client passed in (from JSON) and fetch the old client from the store 367 json = parser.parse(jsonString).getAsJsonObject(); 368 client = gson.fromJson(json, ClientDetailsEntity.class); 369 client = validateSoftwareStatement(client); 370 } catch (JsonSyntaxException e) { 371 logger.error("apiUpdateClient failed due to JsonSyntaxException", e); 372 m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); 373 m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Could not update client. The server encountered a JSON syntax exception. Contact a system administrator for assistance."); 374 return JsonErrorView.VIEWNAME; 375 } catch (IllegalStateException e) { 376 logger.error("apiUpdateClient failed due to IllegalStateException", e); 377 m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); 378 m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Could not update client. The server encountered an IllegalStateException. Refresh and try again - if the problem persists, contact a system administrator for assistance."); 379 return JsonErrorView.VIEWNAME; 380 } catch (ValidationException e) { 381 logger.error("apiUpdateClient failed due to ValidationException", e); 382 m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); 383 m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Could not update client. The server encountered a ValidationException."); 384 return JsonErrorView.VIEWNAME; 385 } 386 387 ClientDetailsEntity oldClient = clientService.getClientById(id); 388 389 if (oldClient == null) { 390 logger.error("apiUpdateClient failed; client with id " + id + " could not be found."); 391 m.addAttribute(HttpCodeView.CODE, HttpStatus.NOT_FOUND); 392 m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Could not update client. The requested client with id " + id + "could not be found."); 393 return JsonErrorView.VIEWNAME; 394 } 395 396 // if they leave the client identifier empty, force it to be generated 397 if (Strings.isNullOrEmpty(client.getClientId())) { 398 client = clientService.generateClientId(client); 399 } 400 401 if (client.getTokenEndpointAuthMethod() == null || 402 client.getTokenEndpointAuthMethod().equals(AuthMethod.NONE)) { 403 // we shouldn't have a secret for this client 404 405 client.setClientSecret(null); 406 407 } else if (client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_BASIC) 408 || client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_POST) 409 || client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_JWT)) { 410 411 // if they've asked for us to generate a client secret (or they left it blank but require one), do so here 412 if (json.has("generateClientSecret") && json.get("generateClientSecret").getAsBoolean() 413 || Strings.isNullOrEmpty(client.getClientSecret())) { 414 client = clientService.generateClientSecret(client); 415 } 416 417 } else if (client.getTokenEndpointAuthMethod().equals(AuthMethod.PRIVATE_KEY)) { 418 419 if (Strings.isNullOrEmpty(client.getJwksUri()) && client.getJwks() == null) { 420 logger.error("tried to create client with private key auth but no private key"); 421 m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); 422 m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Can not create a client with private key authentication without registering a key via the JWK Set URI or JWK Set Value."); 423 return JsonErrorView.VIEWNAME; 424 } 425 426 // otherwise we shouldn't have a secret for this client 427 client.setClientSecret(null); 428 429 } else { 430 431 logger.error("unknown auth method"); 432 m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); 433 m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unknown auth method requested"); 434 return JsonErrorView.VIEWNAME; 435 436 437 } 438 439 try { 440 ClientDetailsEntity newClient = clientService.updateClient(oldClient, client); 441 m.addAttribute(JsonEntityView.ENTITY, newClient); 442 443 if (AuthenticationUtilities.isAdmin(auth)) { 444 return ClientEntityViewForAdmins.VIEWNAME; 445 } else { 446 return ClientEntityViewForUsers.VIEWNAME; 447 } 448 } catch (IllegalArgumentException e) { 449 logger.error("Unable to save client: {}", e.getMessage()); 450 m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); 451 m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unable to save client: " + e.getMessage()); 452 return JsonErrorView.VIEWNAME; 453 } 454 } 455 456 /** 457 * Delete a client 458 * @param id 459 * @param modelAndView 460 * @return 461 */ 462 @PreAuthorize("hasRole('ROLE_ADMIN')") 463 @RequestMapping(value="/{id}", method=RequestMethod.DELETE) 464 public String apiDeleteClient(@PathVariable("id") Long id, ModelAndView modelAndView) { 465 466 ClientDetailsEntity client = clientService.getClientById(id); 467 468 if (client == null) { 469 logger.error("apiDeleteClient failed; client with id " + id + " could not be found."); 470 modelAndView.getModelMap().put(HttpCodeView.CODE, HttpStatus.NOT_FOUND); 471 modelAndView.getModelMap().put(JsonErrorView.ERROR_MESSAGE, "Could not delete client. The requested client with id " + id + "could not be found."); 472 return JsonErrorView.VIEWNAME; 473 } else { 474 modelAndView.getModelMap().put(HttpCodeView.CODE, HttpStatus.OK); 475 clientService.deleteClient(client); 476 } 477 478 return HttpCodeView.VIEWNAME; 479 } 480 481 482 /** 483 * Get an individual client 484 * @param id 485 * @param modelAndView 486 * @return 487 */ 488 @RequestMapping(value="/{id}", method=RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) 489 public String apiShowClient(@PathVariable("id") Long id, Model model, Authentication auth) { 490 491 ClientDetailsEntity client = clientService.getClientById(id); 492 493 if (client == null) { 494 logger.error("apiShowClient failed; client with id " + id + " could not be found."); 495 model.addAttribute(HttpCodeView.CODE, HttpStatus.NOT_FOUND); 496 model.addAttribute(JsonErrorView.ERROR_MESSAGE, "The requested client with id " + id + " could not be found."); 497 return JsonErrorView.VIEWNAME; 498 } 499 500 model.addAttribute(JsonEntityView.ENTITY, client); 501 502 if (AuthenticationUtilities.isAdmin(auth)) { 503 return ClientEntityViewForAdmins.VIEWNAME; 504 } else { 505 return ClientEntityViewForUsers.VIEWNAME; 506 } 507 } 508 509 /** 510 * Get the logo image for a client 511 * @param id 512 */ 513 @RequestMapping(value = "/{id}/logo", method=RequestMethod.GET, produces = { MediaType.IMAGE_GIF_VALUE, MediaType.IMAGE_JPEG_VALUE, MediaType.IMAGE_PNG_VALUE }) 514 public ResponseEntity<byte[]> getClientLogo(@PathVariable("id") Long id, Model model) { 515 516 ClientDetailsEntity client = clientService.getClientById(id); 517 518 if (client == null) { 519 return new ResponseEntity<>(HttpStatus.NOT_FOUND); 520 } else if (Strings.isNullOrEmpty(client.getLogoUri())) { 521 return new ResponseEntity<>(HttpStatus.NOT_FOUND); 522 } else { 523 // get the image from cache 524 CachedImage image = clientLogoLoadingService.getLogo(client); 525 526 HttpHeaders headers = new HttpHeaders(); 527 headers.setContentType(MediaType.parseMediaType(image.getContentType())); 528 headers.setContentLength(image.getLength()); 529 530 return new ResponseEntity<>(image.getData(), headers, HttpStatus.OK); 531 } 532 } 533 534 private ClientDetailsEntity validateSoftwareStatement(ClientDetailsEntity newClient) throws ValidationException { 535 if (newClient.getSoftwareStatement() != null) { 536 if (assertionValidator.isValid(newClient.getSoftwareStatement())) { 537 // we have a software statement and its envelope passed all the checks from our validator 538 539 // swap out all of the client's fields for the associated parts of the software statement 540 try { 541 JWTClaimsSet claimSet = newClient.getSoftwareStatement().getJWTClaimsSet(); 542 for (String claim : claimSet.getClaims().keySet()) { 543 switch (claim) { 544 case SOFTWARE_STATEMENT: 545 throw new ValidationException("invalid_client_metadata", "Software statement can't include another software statement", HttpStatus.BAD_REQUEST); 546 case CLAIMS_REDIRECT_URIS: 547 newClient.setClaimsRedirectUris(Sets.newHashSet(claimSet.getStringListClaim(claim))); 548 break; 549 case CLIENT_SECRET_EXPIRES_AT: 550 throw new ValidationException("invalid_client_metadata", "Software statement can't include a client secret expiration time", HttpStatus.BAD_REQUEST); 551 case CLIENT_ID_ISSUED_AT: 552 throw new ValidationException("invalid_client_metadata", "Software statement can't include a client ID issuance time", HttpStatus.BAD_REQUEST); 553 case REGISTRATION_CLIENT_URI: 554 throw new ValidationException("invalid_client_metadata", "Software statement can't include a client configuration endpoint", HttpStatus.BAD_REQUEST); 555 case REGISTRATION_ACCESS_TOKEN: 556 throw new ValidationException("invalid_client_metadata", "Software statement can't include a client registration access token", HttpStatus.BAD_REQUEST); 557 case REQUEST_URIS: 558 newClient.setRequestUris(Sets.newHashSet(claimSet.getStringListClaim(claim))); 559 break; 560 case POST_LOGOUT_REDIRECT_URIS: 561 newClient.setPostLogoutRedirectUris(Sets.newHashSet(claimSet.getStringListClaim(claim))); 562 break; 563 case INITIATE_LOGIN_URI: 564 newClient.setInitiateLoginUri(claimSet.getStringClaim(claim)); 565 break; 566 case DEFAULT_ACR_VALUES: 567 newClient.setDefaultACRvalues(Sets.newHashSet(claimSet.getStringListClaim(claim))); 568 break; 569 case REQUIRE_AUTH_TIME: 570 newClient.setRequireAuthTime(claimSet.getBooleanClaim(claim)); 571 break; 572 case DEFAULT_MAX_AGE: 573 newClient.setDefaultMaxAge(claimSet.getIntegerClaim(claim)); 574 break; 575 case TOKEN_ENDPOINT_AUTH_SIGNING_ALG: 576 newClient.setTokenEndpointAuthSigningAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim))); 577 break; 578 case ID_TOKEN_ENCRYPTED_RESPONSE_ENC: 579 newClient.setIdTokenEncryptedResponseEnc(EncryptionMethod.parse(claimSet.getStringClaim(claim))); 580 break; 581 case ID_TOKEN_ENCRYPTED_RESPONSE_ALG: 582 newClient.setIdTokenEncryptedResponseAlg(JWEAlgorithm.parse(claimSet.getStringClaim(claim))); 583 break; 584 case ID_TOKEN_SIGNED_RESPONSE_ALG: 585 newClient.setIdTokenSignedResponseAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim))); 586 break; 587 case USERINFO_ENCRYPTED_RESPONSE_ENC: 588 newClient.setUserInfoEncryptedResponseEnc(EncryptionMethod.parse(claimSet.getStringClaim(claim))); 589 break; 590 case USERINFO_ENCRYPTED_RESPONSE_ALG: 591 newClient.setUserInfoEncryptedResponseAlg(JWEAlgorithm.parse(claimSet.getStringClaim(claim))); 592 break; 593 case USERINFO_SIGNED_RESPONSE_ALG: 594 newClient.setUserInfoSignedResponseAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim))); 595 break; 596 case REQUEST_OBJECT_SIGNING_ALG: 597 newClient.setRequestObjectSigningAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim))); 598 break; 599 case SUBJECT_TYPE: 600 newClient.setSubjectType(SubjectType.getByValue(claimSet.getStringClaim(claim))); 601 break; 602 case SECTOR_IDENTIFIER_URI: 603 newClient.setSectorIdentifierUri(claimSet.getStringClaim(claim)); 604 break; 605 case APPLICATION_TYPE: 606 newClient.setApplicationType(AppType.getByValue(claimSet.getStringClaim(claim))); 607 break; 608 case JWKS_URI: 609 newClient.setJwksUri(claimSet.getStringClaim(claim)); 610 break; 611 case JWKS: 612 newClient.setJwks(JWKSet.parse(claimSet.getJSONObjectClaim(claim).toJSONString())); 613 break; 614 case POLICY_URI: 615 newClient.setPolicyUri(claimSet.getStringClaim(claim)); 616 break; 617 case RESPONSE_TYPES: 618 newClient.setResponseTypes(Sets.newHashSet(claimSet.getStringListClaim(claim))); 619 break; 620 case GRANT_TYPES: 621 newClient.setGrantTypes(Sets.newHashSet(claimSet.getStringListClaim(claim))); 622 break; 623 case SCOPE: 624 newClient.setScope(OAuth2Utils.parseParameterList(claimSet.getStringClaim(claim))); 625 break; 626 case TOKEN_ENDPOINT_AUTH_METHOD: 627 newClient.setTokenEndpointAuthMethod(AuthMethod.getByValue(claimSet.getStringClaim(claim))); 628 break; 629 case TOS_URI: 630 newClient.setTosUri(claimSet.getStringClaim(claim)); 631 break; 632 case CONTACTS: 633 newClient.setContacts(Sets.newHashSet(claimSet.getStringListClaim(claim))); 634 break; 635 case LOGO_URI: 636 newClient.setLogoUri(claimSet.getStringClaim(claim)); 637 break; 638 case CLIENT_URI: 639 newClient.setClientUri(claimSet.getStringClaim(claim)); 640 break; 641 case CLIENT_NAME: 642 newClient.setClientName(claimSet.getStringClaim(claim)); 643 break; 644 case REDIRECT_URIS: 645 newClient.setRedirectUris(Sets.newHashSet(claimSet.getStringListClaim(claim))); 646 break; 647 case CLIENT_SECRET: 648 throw new ValidationException("invalid_client_metadata", "Software statement can't contain client secret", HttpStatus.BAD_REQUEST); 649 case CLIENT_ID: 650 throw new ValidationException("invalid_client_metadata", "Software statement can't contain client ID", HttpStatus.BAD_REQUEST); 651 652 default: 653 logger.warn("Software statement contained unknown field: " + claim + " with value " + claimSet.getClaim(claim)); 654 break; 655 } 656 } 657 658 return newClient; 659 } catch (ParseException e) { 660 throw new ValidationException("invalid_client_metadata", "Software statement claims didn't parse", HttpStatus.BAD_REQUEST); 661 } 662 } else { 663 throw new ValidationException("invalid_client_metadata", "Software statement rejected by validator", HttpStatus.BAD_REQUEST); 664 } 665 } else { 666 // nothing to see here, carry on 667 return newClient; 668 } 669 670 } 671 672}