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.oauth2.service.impl; 019 020import java.math.BigInteger; 021import java.security.SecureRandom; 022import java.util.ArrayList; 023import java.util.Collection; 024import java.util.Date; 025import java.util.List; 026import java.util.Set; 027import java.util.UUID; 028import java.util.concurrent.ExecutionException; 029import java.util.concurrent.TimeUnit; 030 031import org.apache.commons.codec.binary.Base64; 032import org.apache.http.client.HttpClient; 033import org.apache.http.impl.client.HttpClientBuilder; 034import org.mitre.oauth2.model.ClientDetailsEntity; 035import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod; 036import org.mitre.oauth2.model.SystemScope; 037import org.mitre.oauth2.repository.OAuth2ClientRepository; 038import org.mitre.oauth2.repository.OAuth2TokenRepository; 039import org.mitre.oauth2.service.ClientDetailsEntityService; 040import org.mitre.oauth2.service.SystemScopeService; 041import org.mitre.openid.connect.config.ConfigurationPropertiesBean; 042import org.mitre.openid.connect.model.WhitelistedSite; 043import org.mitre.openid.connect.service.ApprovedSiteService; 044import org.mitre.openid.connect.service.BlacklistedSiteService; 045import org.mitre.openid.connect.service.StatsService; 046import org.mitre.openid.connect.service.WhitelistedSiteService; 047import org.mitre.uma.model.ResourceSet; 048import org.mitre.uma.service.ResourceSetService; 049import org.slf4j.Logger; 050import org.slf4j.LoggerFactory; 051import org.springframework.beans.factory.annotation.Autowired; 052import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; 053import org.springframework.security.oauth2.common.exceptions.InvalidClientException; 054import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; 055import org.springframework.stereotype.Service; 056import org.springframework.web.client.RestTemplate; 057import org.springframework.web.util.UriComponents; 058import org.springframework.web.util.UriComponentsBuilder; 059 060import com.google.common.base.Strings; 061import com.google.common.cache.CacheBuilder; 062import com.google.common.cache.CacheLoader; 063import com.google.common.cache.LoadingCache; 064import com.google.common.util.concurrent.UncheckedExecutionException; 065import com.google.gson.JsonElement; 066import com.google.gson.JsonParser; 067 068@Service 069public class DefaultOAuth2ClientDetailsEntityService implements ClientDetailsEntityService { 070 071 /** 072 * Logger for this class 073 */ 074 private static final Logger logger = LoggerFactory.getLogger(DefaultOAuth2ClientDetailsEntityService.class); 075 076 @Autowired 077 private OAuth2ClientRepository clientRepository; 078 079 @Autowired 080 private OAuth2TokenRepository tokenRepository; 081 082 @Autowired 083 private ApprovedSiteService approvedSiteService; 084 085 @Autowired 086 private WhitelistedSiteService whitelistedSiteService; 087 088 @Autowired 089 private BlacklistedSiteService blacklistedSiteService; 090 091 @Autowired 092 private SystemScopeService scopeService; 093 094 @Autowired 095 private StatsService statsService; 096 097 @Autowired 098 private ResourceSetService resourceSetService; 099 100 @Autowired 101 private ConfigurationPropertiesBean config; 102 103 // map of sector URI -> list of redirect URIs 104 private LoadingCache<String, List<String>> sectorRedirects = CacheBuilder.newBuilder() 105 .expireAfterAccess(1, TimeUnit.HOURS) 106 .maximumSize(100) 107 .build(new SectorIdentifierLoader(HttpClientBuilder.create().useSystemProperties().build())); 108 109 @Override 110 public ClientDetailsEntity saveNewClient(ClientDetailsEntity client) { 111 if (client.getId() != null) { // if it's not null, it's already been saved, this is an error 112 throw new IllegalArgumentException("Tried to save a new client with an existing ID: " + client.getId()); 113 } 114 115 if (client.getRegisteredRedirectUri() != null) { 116 for (String uri : client.getRegisteredRedirectUri()) { 117 if (blacklistedSiteService.isBlacklisted(uri)) { 118 throw new IllegalArgumentException("Client URI is blacklisted: " + uri); 119 } 120 } 121 } 122 123 // assign a random clientid if it's empty 124 // NOTE: don't assign a random client secret without asking, since public clients have no secret 125 if (Strings.isNullOrEmpty(client.getClientId())) { 126 client = generateClientId(client); 127 } 128 129 // make sure that clients with the "refresh_token" grant type have the "offline_access" scope, and vice versa 130 ensureRefreshTokenConsistency(client); 131 132 // make sure we don't have both a JWKS and a JWKS URI 133 ensureKeyConsistency(client); 134 135 // check consistency when using HEART mode 136 checkHeartMode(client); 137 138 // timestamp this to right now 139 client.setCreatedAt(new Date()); 140 141 142 // check the sector URI 143 checkSectorIdentifierUri(client); 144 145 146 ensureNoReservedScopes(client); 147 148 ClientDetailsEntity c = clientRepository.saveClient(client); 149 150 statsService.resetCache(); 151 152 return c; 153 } 154 155 /** 156 * Make sure the client has only one type of key registered 157 * @param client 158 */ 159 private void ensureKeyConsistency(ClientDetailsEntity client) { 160 if (client.getJwksUri() != null && client.getJwks() != null) { 161 // a client can only have one key type or the other, not both 162 throw new IllegalArgumentException("A client cannot have both JWKS URI and JWKS value"); 163 } 164 } 165 166 /** 167 * Make sure the client doesn't request any system reserved scopes 168 */ 169 private void ensureNoReservedScopes(ClientDetailsEntity client) { 170 // make sure a client doesn't get any special system scopes 171 Set<SystemScope> requestedScope = scopeService.fromStrings(client.getScope()); 172 173 requestedScope = scopeService.removeReservedScopes(requestedScope); 174 175 client.setScope(scopeService.toStrings(requestedScope)); 176 } 177 178 /** 179 * Load the sector identifier URI if it exists and check the redirect URIs against it 180 * @param client 181 */ 182 private void checkSectorIdentifierUri(ClientDetailsEntity client) { 183 if (!Strings.isNullOrEmpty(client.getSectorIdentifierUri())) { 184 try { 185 List<String> redirects = sectorRedirects.get(client.getSectorIdentifierUri()); 186 187 if (client.getRegisteredRedirectUri() != null) { 188 for (String uri : client.getRegisteredRedirectUri()) { 189 if (!redirects.contains(uri)) { 190 throw new IllegalArgumentException("Requested Redirect URI " + uri + " is not listed at sector identifier " + redirects); 191 } 192 } 193 } 194 195 } catch (UncheckedExecutionException | ExecutionException e) { 196 throw new IllegalArgumentException("Unable to load sector identifier URI " + client.getSectorIdentifierUri() + ": " + e.getMessage()); 197 } 198 } 199 } 200 201 /** 202 * Make sure the client has the appropriate scope and grant type. 203 * @param client 204 */ 205 private void ensureRefreshTokenConsistency(ClientDetailsEntity client) { 206 if (client.getAuthorizedGrantTypes().contains("refresh_token") 207 || client.getScope().contains(SystemScopeService.OFFLINE_ACCESS)) { 208 client.getScope().add(SystemScopeService.OFFLINE_ACCESS); 209 client.getAuthorizedGrantTypes().add("refresh_token"); 210 } 211 } 212 213 /** 214 * If HEART mode is enabled, make sure the client meets the requirements: 215 * - Only one of authorization_code, implicit, or client_credentials can be used at a time 216 * - A redirect_uri must be registered with either authorization_code or implicit 217 * - A key must be registered 218 * - A client secret must not be generated 219 * - authorization_code and client_credentials must use the private_key authorization method 220 * @param client 221 */ 222 private void checkHeartMode(ClientDetailsEntity client) { 223 if (config.isHeartMode()) { 224 if (client.getGrantTypes().contains("authorization_code")) { 225 // make sure we don't have incompatible grant types 226 if (client.getGrantTypes().contains("implicit") || client.getGrantTypes().contains("client_credentials")) { 227 throw new IllegalArgumentException("[HEART mode] Incompatible grant types"); 228 } 229 230 // make sure we've got the right authentication method 231 if (client.getTokenEndpointAuthMethod() == null || !client.getTokenEndpointAuthMethod().equals(AuthMethod.PRIVATE_KEY)) { 232 throw new IllegalArgumentException("[HEART mode] Authorization code clients must use the private_key authentication method"); 233 } 234 235 // make sure we've got a redirect URI 236 if (client.getRedirectUris().isEmpty()) { 237 throw new IllegalArgumentException("[HEART mode] Authorization code clients must register at least one redirect URI"); 238 } 239 } 240 241 if (client.getGrantTypes().contains("implicit")) { 242 // make sure we don't have incompatible grant types 243 if (client.getGrantTypes().contains("authorization_code") || client.getGrantTypes().contains("client_credentials") || client.getGrantTypes().contains("refresh_token")) { 244 throw new IllegalArgumentException("[HEART mode] Incompatible grant types"); 245 } 246 247 // make sure we've got the right authentication method 248 if (client.getTokenEndpointAuthMethod() == null || !client.getTokenEndpointAuthMethod().equals(AuthMethod.NONE)) { 249 throw new IllegalArgumentException("[HEART mode] Implicit clients must use the none authentication method"); 250 } 251 252 // make sure we've got a redirect URI 253 if (client.getRedirectUris().isEmpty()) { 254 throw new IllegalArgumentException("[HEART mode] Implicit clients must register at least one redirect URI"); 255 } 256 } 257 258 if (client.getGrantTypes().contains("client_credentials")) { 259 // make sure we don't have incompatible grant types 260 if (client.getGrantTypes().contains("authorization_code") || client.getGrantTypes().contains("implicit") || client.getGrantTypes().contains("refresh_token")) { 261 throw new IllegalArgumentException("[HEART mode] Incompatible grant types"); 262 } 263 264 // make sure we've got the right authentication method 265 if (client.getTokenEndpointAuthMethod() == null || !client.getTokenEndpointAuthMethod().equals(AuthMethod.PRIVATE_KEY)) { 266 throw new IllegalArgumentException("[HEART mode] Client credentials clients must use the private_key authentication method"); 267 } 268 269 // make sure we've got a redirect URI 270 if (!client.getRedirectUris().isEmpty()) { 271 throw new IllegalArgumentException("[HEART mode] Client credentials clients must not register a redirect URI"); 272 } 273 274 } 275 276 if (client.getGrantTypes().contains("password")) { 277 throw new IllegalArgumentException("[HEART mode] Password grant type is forbidden"); 278 } 279 280 // make sure we don't have a client secret 281 if (!Strings.isNullOrEmpty(client.getClientSecret())) { 282 throw new IllegalArgumentException("[HEART mode] Client secrets are not allowed"); 283 } 284 285 // make sure we've got a key registered 286 if (client.getJwks() == null && Strings.isNullOrEmpty(client.getJwksUri())) { 287 throw new IllegalArgumentException("[HEART mode] All clients must have a key registered"); 288 } 289 290 // make sure our redirect URIs each fit one of the allowed categories 291 if (client.getRedirectUris() != null && !client.getRedirectUris().isEmpty()) { 292 boolean localhost = false; 293 boolean remoteHttps = false; 294 boolean customScheme = false; 295 for (String uri : client.getRedirectUris()) { 296 UriComponents components = UriComponentsBuilder.fromUriString(uri).build(); 297 if (components.getScheme() == null) { 298 // this is a very unknown redirect URI 299 customScheme = true; 300 } else if (components.getScheme().equals("http")) { 301 // http scheme, check for localhost 302 if (components.getHost().equals("localhost") || components.getHost().equals("127.0.0.1")) { 303 localhost = true; 304 } else { 305 throw new IllegalArgumentException("[HEART mode] Can't have an http redirect URI on non-local host"); 306 } 307 } else if (components.getScheme().equals("https")) { 308 remoteHttps = true; 309 } else { 310 customScheme = true; 311 } 312 } 313 314 // now we make sure the client has a URI in only one of each of the three categories 315 if (!((localhost ^ remoteHttps ^ customScheme) 316 && !(localhost && remoteHttps && customScheme))) { 317 throw new IllegalArgumentException("[HEART mode] Can't have more than one class of redirect URI"); 318 } 319 } 320 321 } 322 } 323 324 /** 325 * Get the client by its internal ID 326 */ 327 @Override 328 public ClientDetailsEntity getClientById(Long id) { 329 ClientDetailsEntity client = clientRepository.getById(id); 330 331 return client; 332 } 333 334 /** 335 * Get the client for the given ClientID 336 */ 337 @Override 338 public ClientDetailsEntity loadClientByClientId(String clientId) throws OAuth2Exception, InvalidClientException, IllegalArgumentException { 339 if (!Strings.isNullOrEmpty(clientId)) { 340 ClientDetailsEntity client = clientRepository.getClientByClientId(clientId); 341 if (client == null) { 342 throw new InvalidClientException("Client with id " + clientId + " was not found"); 343 } 344 else { 345 return client; 346 } 347 } 348 349 throw new IllegalArgumentException("Client id must not be empty!"); 350 } 351 352 /** 353 * Delete a client and all its associated tokens 354 */ 355 @Override 356 public void deleteClient(ClientDetailsEntity client) throws InvalidClientException { 357 358 if (clientRepository.getById(client.getId()) == null) { 359 throw new InvalidClientException("Client with id " + client.getClientId() + " was not found"); 360 } 361 362 // clean out any tokens that this client had issued 363 tokenRepository.clearTokensForClient(client); 364 365 // clean out any approved sites for this client 366 approvedSiteService.clearApprovedSitesForClient(client); 367 368 // clear out any whitelisted sites for this client 369 WhitelistedSite whitelistedSite = whitelistedSiteService.getByClientId(client.getClientId()); 370 if (whitelistedSite != null) { 371 whitelistedSiteService.remove(whitelistedSite); 372 } 373 374 // clear out resource sets registered for this client 375 Collection<ResourceSet> resourceSets = resourceSetService.getAllForClient(client); 376 for (ResourceSet rs : resourceSets) { 377 resourceSetService.remove(rs); 378 } 379 380 // take care of the client itself 381 clientRepository.deleteClient(client); 382 383 statsService.resetCache(); 384 385 } 386 387 /** 388 * Update the oldClient with information from the newClient. The 389 * id from oldClient is retained. 390 * 391 * Checks to make sure the refresh grant type and 392 * the scopes are set appropriately. 393 * 394 * Checks to make sure the redirect URIs aren't blacklisted. 395 * 396 * Attempts to load the redirect URI (possibly cached) to check the 397 * sector identifier against the contents there. 398 * 399 * 400 */ 401 @Override 402 public ClientDetailsEntity updateClient(ClientDetailsEntity oldClient, ClientDetailsEntity newClient) throws IllegalArgumentException { 403 if (oldClient != null && newClient != null) { 404 405 for (String uri : newClient.getRegisteredRedirectUri()) { 406 if (blacklistedSiteService.isBlacklisted(uri)) { 407 throw new IllegalArgumentException("Client URI is blacklisted: " + uri); 408 } 409 } 410 411 // if the client is flagged to allow for refresh tokens, make sure it's got the right scope 412 ensureRefreshTokenConsistency(newClient); 413 414 // make sure we don't have both a JWKS and a JWKS URI 415 ensureKeyConsistency(newClient); 416 417 // check consistency when using HEART mode 418 checkHeartMode(newClient); 419 420 // check the sector URI 421 checkSectorIdentifierUri(newClient); 422 423 // make sure a client doesn't get any special system scopes 424 ensureNoReservedScopes(newClient); 425 426 return clientRepository.updateClient(oldClient.getId(), newClient); 427 } 428 throw new IllegalArgumentException("Neither old client or new client can be null!"); 429 } 430 431 /** 432 * Get all clients in the system 433 */ 434 @Override 435 public Collection<ClientDetailsEntity> getAllClients() { 436 return clientRepository.getAllClients(); 437 } 438 439 /** 440 * Generates a clientId for the given client and sets it to the client's clientId field. Returns the client that was passed in, now with id set. 441 */ 442 @Override 443 public ClientDetailsEntity generateClientId(ClientDetailsEntity client) { 444 client.setClientId(UUID.randomUUID().toString()); 445 return client; 446 } 447 448 /** 449 * Generates a new clientSecret for the given client and sets it to the client's clientSecret field. Returns the client that was passed in, now with secret set. 450 */ 451 @Override 452 public ClientDetailsEntity generateClientSecret(ClientDetailsEntity client) { 453 if (config.isHeartMode()) { 454 logger.error("[HEART mode] Can't generate a client secret, skipping step; client won't be saved due to invalid configuration"); 455 client.setClientSecret(null); 456 } else { 457 client.setClientSecret(Base64.encodeBase64URLSafeString(new BigInteger(512, new SecureRandom()).toByteArray()).replace("=", "")); 458 } 459 return client; 460 } 461 462 /** 463 * Utility class to load a sector identifier's set of authorized redirect URIs. 464 * 465 * @author jricher 466 * 467 */ 468 private class SectorIdentifierLoader extends CacheLoader<String, List<String>> { 469 private HttpComponentsClientHttpRequestFactory httpFactory; 470 private RestTemplate restTemplate; 471 private JsonParser parser = new JsonParser(); 472 473 SectorIdentifierLoader(HttpClient httpClient) { 474 this.httpFactory = new HttpComponentsClientHttpRequestFactory(httpClient); 475 this.restTemplate = new RestTemplate(httpFactory); 476 } 477 478 @Override 479 public List<String> load(String key) throws Exception { 480 481 if (!key.startsWith("https")) { 482 if (config.isForceHttps()) { 483 throw new IllegalArgumentException("Sector identifier must start with https: " + key); 484 } 485 logger.error("Sector identifier doesn't start with https, loading anyway..."); 486 } 487 488 // key is the sector URI 489 String jsonString = restTemplate.getForObject(key, String.class); 490 JsonElement json = parser.parse(jsonString); 491 492 if (json.isJsonArray()) { 493 List<String> redirectUris = new ArrayList<>(); 494 for (JsonElement el : json.getAsJsonArray()) { 495 redirectUris.add(el.getAsString()); 496 } 497 498 logger.info("Found " + redirectUris + " for sector " + key); 499 500 return redirectUris; 501 } else { 502 throw new IllegalArgumentException("JSON Format Error"); 503 } 504 505 } 506 507 } 508 509}