001/******************************************************************************* 002 * Copyright 2017 The MIT Internet Trust Consortium 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 *******************************************************************************/ 016package org.mitre.openid.connect.web; 017 018import java.io.UnsupportedEncodingException; 019import java.text.ParseException; 020import java.util.Date; 021import java.util.HashSet; 022import java.util.Set; 023 024import org.mitre.oauth2.model.ClientDetailsEntity; 025import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod; 026import org.mitre.oauth2.model.OAuth2AccessTokenEntity; 027import org.mitre.oauth2.model.RegisteredClient; 028import org.mitre.oauth2.model.SystemScope; 029import org.mitre.oauth2.service.ClientDetailsEntityService; 030import org.mitre.oauth2.service.OAuth2TokenEntityService; 031import org.mitre.oauth2.service.SystemScopeService; 032import org.mitre.openid.connect.ClientDetailsEntityJsonProcessor; 033import org.mitre.openid.connect.config.ConfigurationPropertiesBean; 034import org.mitre.openid.connect.exception.ValidationException; 035import org.mitre.openid.connect.service.OIDCTokenService; 036import org.mitre.openid.connect.view.ClientInformationResponseView; 037import org.mitre.openid.connect.view.HttpCodeView; 038import org.mitre.openid.connect.view.JsonErrorView; 039import org.slf4j.Logger; 040import org.slf4j.LoggerFactory; 041import org.springframework.beans.factory.annotation.Autowired; 042import org.springframework.http.HttpStatus; 043import org.springframework.http.MediaType; 044import org.springframework.security.access.prepost.PreAuthorize; 045import org.springframework.security.oauth2.provider.OAuth2Authentication; 046import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails; 047import org.springframework.stereotype.Controller; 048import org.springframework.ui.Model; 049import org.springframework.web.bind.annotation.PathVariable; 050import org.springframework.web.bind.annotation.RequestBody; 051import org.springframework.web.bind.annotation.RequestMapping; 052import org.springframework.web.bind.annotation.RequestMethod; 053import org.springframework.web.util.UriUtils; 054 055import com.google.common.base.Strings; 056import com.google.gson.JsonSyntaxException; 057 058@Controller 059@RequestMapping(value = ProtectedResourceRegistrationEndpoint.URL) 060public class ProtectedResourceRegistrationEndpoint { 061 062 /** 063 * 064 */ 065 public static final String URL = "resource"; 066 067 @Autowired 068 private ClientDetailsEntityService clientService; 069 070 @Autowired 071 private OAuth2TokenEntityService tokenService; 072 073 @Autowired 074 private SystemScopeService scopeService; 075 076 @Autowired 077 private ConfigurationPropertiesBean config; 078 079 @Autowired 080 private OIDCTokenService connectTokenService; 081 082 /** 083 * Logger for this class 084 */ 085 private static final Logger logger = LoggerFactory.getLogger(ProtectedResourceRegistrationEndpoint.class); 086 087 /** 088 * Create a new Client, issue a client ID, and create a registration access token. 089 * @param jsonString 090 * @param m 091 * @param p 092 * @return 093 */ 094 @RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) 095 public String registerNewProtectedResource(@RequestBody String jsonString, Model m) { 096 097 ClientDetailsEntity newClient = null; 098 try { 099 newClient = ClientDetailsEntityJsonProcessor.parse(jsonString); 100 } catch (JsonSyntaxException e) { 101 // bad parse 102 // didn't parse, this is a bad request 103 logger.error("registerNewProtectedResource failed; submitted JSON is malformed"); 104 m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400 105 return HttpCodeView.VIEWNAME; 106 } 107 108 if (newClient != null) { 109 // it parsed! 110 111 // 112 // Now do some post-processing consistency checks on it 113 // 114 115 // clear out any spurious id/secret (clients don't get to pick) 116 newClient.setClientId(null); 117 newClient.setClientSecret(null); 118 119 // do validation on the fields 120 try { 121 newClient = validateScopes(newClient); 122 newClient = validateAuth(newClient); 123 } catch (ValidationException ve) { 124 // validation failed, return an error 125 m.addAttribute(JsonErrorView.ERROR, ve.getError()); 126 m.addAttribute(JsonErrorView.ERROR_MESSAGE, ve.getErrorDescription()); 127 m.addAttribute(HttpCodeView.CODE, ve.getStatus()); 128 return JsonErrorView.VIEWNAME; 129 } 130 131 132 // no grant types are allowed 133 newClient.setGrantTypes(new HashSet<String>()); 134 newClient.setResponseTypes(new HashSet<String>()); 135 newClient.setRedirectUris(new HashSet<String>()); 136 137 // don't issue tokens to this client 138 newClient.setAccessTokenValiditySeconds(0); 139 newClient.setIdTokenValiditySeconds(0); 140 newClient.setRefreshTokenValiditySeconds(0); 141 142 // clear out unused fields 143 newClient.setDefaultACRvalues(new HashSet<String>()); 144 newClient.setDefaultMaxAge(null); 145 newClient.setIdTokenEncryptedResponseAlg(null); 146 newClient.setIdTokenEncryptedResponseEnc(null); 147 newClient.setIdTokenSignedResponseAlg(null); 148 newClient.setInitiateLoginUri(null); 149 newClient.setPostLogoutRedirectUris(null); 150 newClient.setRequestObjectSigningAlg(null); 151 newClient.setRequireAuthTime(null); 152 newClient.setReuseRefreshToken(false); 153 newClient.setSectorIdentifierUri(null); 154 newClient.setSubjectType(null); 155 newClient.setUserInfoEncryptedResponseAlg(null); 156 newClient.setUserInfoEncryptedResponseEnc(null); 157 newClient.setUserInfoSignedResponseAlg(null); 158 159 // this client has been dynamically registered (obviously) 160 newClient.setDynamicallyRegistered(true); 161 162 // this client has access to the introspection endpoint 163 newClient.setAllowIntrospection(true); 164 165 // now save it 166 try { 167 ClientDetailsEntity savedClient = clientService.saveNewClient(newClient); 168 169 // generate the registration access token 170 OAuth2AccessTokenEntity token = connectTokenService.createResourceAccessToken(savedClient); 171 tokenService.saveAccessToken(token); 172 173 // send it all out to the view 174 175 RegisteredClient registered = new RegisteredClient(savedClient, token.getValue(), config.getIssuer() + "resource/" + UriUtils.encodePathSegment(savedClient.getClientId(), "UTF-8")); 176 m.addAttribute("client", registered); 177 m.addAttribute(HttpCodeView.CODE, HttpStatus.CREATED); // http 201 178 179 return ClientInformationResponseView.VIEWNAME; 180 } catch (UnsupportedEncodingException e) { 181 logger.error("Unsupported encoding", e); 182 m.addAttribute(HttpCodeView.CODE, HttpStatus.INTERNAL_SERVER_ERROR); 183 return HttpCodeView.VIEWNAME; 184 } catch (IllegalArgumentException e) { 185 logger.error("Couldn't save client", e); 186 187 m.addAttribute(JsonErrorView.ERROR, "invalid_client_metadata"); 188 m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unable to save client due to invalid or inconsistent metadata."); 189 m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400 190 191 return JsonErrorView.VIEWNAME; 192 } 193 } else { 194 // didn't parse, this is a bad request 195 logger.error("registerNewClient failed; submitted JSON is malformed"); 196 m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400 197 198 return HttpCodeView.VIEWNAME; 199 } 200 201 } 202 203 private ClientDetailsEntity validateScopes(ClientDetailsEntity newClient) throws ValidationException { 204 // scopes that the client is asking for 205 Set<SystemScope> requestedScopes = scopeService.fromStrings(newClient.getScope()); 206 207 // the scopes that the client can have must be a subset of the dynamically allowed scopes 208 Set<SystemScope> allowedScopes = scopeService.removeRestrictedAndReservedScopes(requestedScopes); 209 210 // if the client didn't ask for any, give them the defaults 211 if (allowedScopes == null || allowedScopes.isEmpty()) { 212 allowedScopes = scopeService.getDefaults(); 213 } 214 215 newClient.setScope(scopeService.toStrings(allowedScopes)); 216 217 return newClient; 218 } 219 220 /** 221 * Get the meta information for a client. 222 * @param clientId 223 * @param m 224 * @param auth 225 * @return 226 */ 227 @PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + SystemScopeService.RESOURCE_TOKEN_SCOPE + "')") 228 @RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) 229 public String readResourceConfiguration(@PathVariable("id") String clientId, Model m, OAuth2Authentication auth) { 230 231 ClientDetailsEntity client = clientService.loadClientByClientId(clientId); 232 233 if (client != null && client.getClientId().equals(auth.getOAuth2Request().getClientId())) { 234 235 236 237 try { 238 // possibly update the token 239 OAuth2AccessTokenEntity token = fetchValidRegistrationToken(auth, client); 240 241 RegisteredClient registered = new RegisteredClient(client, token.getValue(), config.getIssuer() + "resource/" + UriUtils.encodePathSegment(client.getClientId(), "UTF-8")); 242 243 // send it all out to the view 244 m.addAttribute("client", registered); 245 m.addAttribute(HttpCodeView.CODE, HttpStatus.OK); // http 200 246 247 return ClientInformationResponseView.VIEWNAME; 248 } catch (UnsupportedEncodingException e) { 249 logger.error("Unsupported encoding", e); 250 m.addAttribute(HttpCodeView.CODE, HttpStatus.INTERNAL_SERVER_ERROR); 251 return HttpCodeView.VIEWNAME; 252 } 253 } else { 254 // client mismatch 255 logger.error("readResourceConfiguration failed, client ID mismatch: " 256 + clientId + " and " + auth.getOAuth2Request().getClientId() + " do not match."); 257 m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); // http 403 258 259 return HttpCodeView.VIEWNAME; 260 } 261 } 262 263 /** 264 * Update the metainformation for a given client. 265 * @param clientId 266 * @param jsonString 267 * @param m 268 * @param auth 269 * @return 270 */ 271 @PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + SystemScopeService.RESOURCE_TOKEN_SCOPE + "')") 272 @RequestMapping(value = "/{id}", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) 273 public String updateProtectedResource(@PathVariable("id") String clientId, @RequestBody String jsonString, Model m, OAuth2Authentication auth) { 274 275 276 ClientDetailsEntity newClient = null; 277 try { 278 newClient = ClientDetailsEntityJsonProcessor.parse(jsonString); 279 } catch (JsonSyntaxException e) { 280 // bad parse 281 // didn't parse, this is a bad request 282 logger.error("updateProtectedResource failed; submitted JSON is malformed"); 283 m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400 284 return HttpCodeView.VIEWNAME; 285 } 286 287 ClientDetailsEntity oldClient = clientService.loadClientByClientId(clientId); 288 289 if (newClient != null && oldClient != null // we have an existing client and the new one parsed 290 && oldClient.getClientId().equals(auth.getOAuth2Request().getClientId()) // the client passed in the URI matches the one in the auth 291 && oldClient.getClientId().equals(newClient.getClientId()) // the client passed in the body matches the one in the URI 292 ) { 293 294 // a client can't ask to update its own client secret to any particular value 295 newClient.setClientSecret(oldClient.getClientSecret()); 296 297 newClient.setCreatedAt(oldClient.getCreatedAt()); 298 299 // no grant types are allowed 300 newClient.setGrantTypes(new HashSet<String>()); 301 newClient.setResponseTypes(new HashSet<String>()); 302 newClient.setRedirectUris(new HashSet<String>()); 303 304 // don't issue tokens to this client 305 newClient.setAccessTokenValiditySeconds(0); 306 newClient.setIdTokenValiditySeconds(0); 307 newClient.setRefreshTokenValiditySeconds(0); 308 309 // clear out unused fields 310 newClient.setDefaultACRvalues(new HashSet<String>()); 311 newClient.setDefaultMaxAge(null); 312 newClient.setIdTokenEncryptedResponseAlg(null); 313 newClient.setIdTokenEncryptedResponseEnc(null); 314 newClient.setIdTokenSignedResponseAlg(null); 315 newClient.setInitiateLoginUri(null); 316 newClient.setPostLogoutRedirectUris(null); 317 newClient.setRequestObjectSigningAlg(null); 318 newClient.setRequireAuthTime(null); 319 newClient.setReuseRefreshToken(false); 320 newClient.setSectorIdentifierUri(null); 321 newClient.setSubjectType(null); 322 newClient.setUserInfoEncryptedResponseAlg(null); 323 newClient.setUserInfoEncryptedResponseEnc(null); 324 newClient.setUserInfoSignedResponseAlg(null); 325 326 // this client has been dynamically registered (obviously) 327 newClient.setDynamicallyRegistered(true); 328 329 // this client has access to the introspection endpoint 330 newClient.setAllowIntrospection(true); 331 332 // do validation on the fields 333 try { 334 newClient = validateScopes(newClient); 335 newClient = validateAuth(newClient); 336 } catch (ValidationException ve) { 337 // validation failed, return an error 338 m.addAttribute(JsonErrorView.ERROR, ve.getError()); 339 m.addAttribute(JsonErrorView.ERROR_MESSAGE, ve.getErrorDescription()); 340 m.addAttribute(HttpCodeView.CODE, ve.getStatus()); 341 return JsonErrorView.VIEWNAME; 342 } 343 344 345 try { 346 // save the client 347 ClientDetailsEntity savedClient = clientService.updateClient(oldClient, newClient); 348 349 // possibly update the token 350 OAuth2AccessTokenEntity token = fetchValidRegistrationToken(auth, savedClient); 351 352 RegisteredClient registered = new RegisteredClient(savedClient, token.getValue(), config.getIssuer() + "resource/" + UriUtils.encodePathSegment(savedClient.getClientId(), "UTF-8")); 353 354 // send it all out to the view 355 m.addAttribute("client", registered); 356 m.addAttribute(HttpCodeView.CODE, HttpStatus.OK); // http 200 357 358 return ClientInformationResponseView.VIEWNAME; 359 } catch (UnsupportedEncodingException e) { 360 logger.error("Unsupported encoding", e); 361 m.addAttribute(HttpCodeView.CODE, HttpStatus.INTERNAL_SERVER_ERROR); 362 return HttpCodeView.VIEWNAME; 363 } catch (IllegalArgumentException e) { 364 logger.error("Couldn't save client", e); 365 366 m.addAttribute(JsonErrorView.ERROR, "invalid_client_metadata"); 367 m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unable to save client due to invalid or inconsistent metadata."); 368 m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400 369 370 return JsonErrorView.VIEWNAME; 371 } 372 } else { 373 // client mismatch 374 logger.error("updateProtectedResource" + 375 " failed, client ID mismatch: " 376 + clientId + " and " + auth.getOAuth2Request().getClientId() + " do not match."); 377 m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); // http 403 378 379 return HttpCodeView.VIEWNAME; 380 } 381 } 382 383 /** 384 * Delete the indicated client from the system. 385 * @param clientId 386 * @param m 387 * @param auth 388 * @return 389 */ 390 @PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + SystemScopeService.RESOURCE_TOKEN_SCOPE + "')") 391 @RequestMapping(value = "/{id}", method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_VALUE) 392 public String deleteResource(@PathVariable("id") String clientId, Model m, OAuth2Authentication auth) { 393 394 ClientDetailsEntity client = clientService.loadClientByClientId(clientId); 395 396 if (client != null && client.getClientId().equals(auth.getOAuth2Request().getClientId())) { 397 398 clientService.deleteClient(client); 399 400 m.addAttribute(HttpCodeView.CODE, HttpStatus.NO_CONTENT); // http 204 401 402 return HttpCodeView.VIEWNAME; 403 } else { 404 // client mismatch 405 logger.error("readClientConfiguration failed, client ID mismatch: " 406 + clientId + " and " + auth.getOAuth2Request().getClientId() + " do not match."); 407 m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); // http 403 408 409 return HttpCodeView.VIEWNAME; 410 } 411 } 412 413 private ClientDetailsEntity validateAuth(ClientDetailsEntity newClient) throws ValidationException { 414 if (newClient.getTokenEndpointAuthMethod() == null) { 415 newClient.setTokenEndpointAuthMethod(AuthMethod.SECRET_BASIC); 416 } 417 418 if (newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_BASIC || 419 newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_JWT || 420 newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_POST) { 421 422 if (Strings.isNullOrEmpty(newClient.getClientSecret())) { 423 // no secret yet, we need to generate a secret 424 newClient = clientService.generateClientSecret(newClient); 425 } 426 } else if (newClient.getTokenEndpointAuthMethod() == AuthMethod.PRIVATE_KEY) { 427 if (Strings.isNullOrEmpty(newClient.getJwksUri()) && newClient.getJwks() == null) { 428 throw new ValidationException("invalid_client_metadata", "JWK Set URI required when using private key authentication", HttpStatus.BAD_REQUEST); 429 } 430 431 newClient.setClientSecret(null); 432 } else if (newClient.getTokenEndpointAuthMethod() == AuthMethod.NONE) { 433 newClient.setClientSecret(null); 434 } else { 435 throw new ValidationException("invalid_client_metadata", "Unknown authentication method", HttpStatus.BAD_REQUEST); 436 } 437 return newClient; 438 } 439 440 private OAuth2AccessTokenEntity fetchValidRegistrationToken(OAuth2Authentication auth, ClientDetailsEntity client) { 441 442 OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) auth.getDetails(); 443 OAuth2AccessTokenEntity token = tokenService.readAccessToken(details.getTokenValue()); 444 445 if (config.getRegTokenLifeTime() != null) { 446 447 try { 448 // Re-issue the token if it has been issued before [currentTime - validity] 449 Date validToDate = new Date(System.currentTimeMillis() - config.getRegTokenLifeTime() * 1000); 450 if(token.getJwt().getJWTClaimsSet().getIssueTime().before(validToDate)) { 451 logger.info("Rotating the registration access token for " + client.getClientId()); 452 tokenService.revokeAccessToken(token); 453 OAuth2AccessTokenEntity newToken = connectTokenService.createResourceAccessToken(client); 454 tokenService.saveAccessToken(newToken); 455 return newToken; 456 } else { 457 // it's not expired, keep going 458 return token; 459 } 460 } catch (ParseException e) { 461 logger.error("Couldn't parse a known-valid token?", e); 462 return token; 463 } 464 } else { 465 // tokens don't expire, just return it 466 return token; 467 } 468 } 469 470}