001package com.box.sdk; 002 003import com.eclipsesource.json.Json; 004import com.eclipsesource.json.JsonObject; 005import java.io.IOException; 006import java.io.StringReader; 007import java.net.MalformedURLException; 008import java.net.URL; 009import java.security.PrivateKey; 010import java.security.Security; 011import java.text.ParseException; 012import java.text.SimpleDateFormat; 013import java.util.Date; 014import java.util.List; 015import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; 016import org.bouncycastle.jce.provider.BouncyCastleProvider; 017import org.bouncycastle.openssl.PEMDecryptorProvider; 018import org.bouncycastle.openssl.PEMEncryptedKeyPair; 019import org.bouncycastle.openssl.PEMKeyPair; 020import org.bouncycastle.openssl.PEMParser; 021import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; 022import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder; 023import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder; 024import org.bouncycastle.operator.InputDecryptorProvider; 025import org.bouncycastle.operator.OperatorCreationException; 026import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo; 027import org.bouncycastle.pkcs.PKCSException; 028import org.jose4j.jws.AlgorithmIdentifiers; 029import org.jose4j.jws.JsonWebSignature; 030import org.jose4j.jwt.JwtClaims; 031import org.jose4j.jwt.NumericDate; 032import org.jose4j.lang.JoseException; 033 034/** 035 * Represents an authenticated Box Developer Edition connection to the Box API. 036 * 037 * <p>This class handles everything for Box Developer Edition that isn't already handled by BoxAPIConnection.</p> 038 */ 039public class BoxDeveloperEditionAPIConnection extends BoxAPIConnection { 040 041 private static final String JWT_AUDIENCE = "https://api.box.com/oauth2/token"; 042 private static final String JWT_GRANT_TYPE = 043 "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&client_id=%s&client_secret=%s&assertion=%s"; 044 private static final int DEFAULT_MAX_ENTRIES = 100; 045 046 static { 047 Security.addProvider(new BouncyCastleProvider()); 048 } 049 050 private final String entityID; 051 private final DeveloperEditionEntityType entityType; 052 private final EncryptionAlgorithm encryptionAlgorithm; 053 private final String publicKeyID; 054 private final String privateKey; 055 private final String privateKeyPassword; 056 private BackoffCounter backoffCounter; 057 private final IAccessTokenCache accessTokenCache; 058 059 /** 060 * Constructs a new BoxDeveloperEditionAPIConnection leveraging an access token cache. 061 * 062 * @param entityId enterprise ID or a user ID. 063 * @param entityType the type of entityId. 064 * @param clientID the client ID to use when exchanging the JWT assertion for an access token. 065 * @param clientSecret the client secret to use when exchanging the JWT assertion for an access token. 066 * @param encryptionPref the encryption preferences for signing the JWT. 067 * @param accessTokenCache the cache for storing access token information (to minimize fetching new tokens) 068 */ 069 public BoxDeveloperEditionAPIConnection(String entityId, DeveloperEditionEntityType entityType, 070 String clientID, String clientSecret, 071 JWTEncryptionPreferences encryptionPref, 072 IAccessTokenCache accessTokenCache) { 073 074 super(clientID, clientSecret); 075 076 this.entityID = entityId; 077 this.entityType = entityType; 078 this.publicKeyID = encryptionPref.getPublicKeyID(); 079 this.privateKey = encryptionPref.getPrivateKey(); 080 this.privateKeyPassword = encryptionPref.getPrivateKeyPassword(); 081 this.encryptionAlgorithm = encryptionPref.getEncryptionAlgorithm(); 082 this.accessTokenCache = accessTokenCache; 083 this.backoffCounter = new BackoffCounter(new Time()); 084 } 085 086 /** 087 * Constructs a new BoxDeveloperEditionAPIConnection. 088 * Uses {@link InMemoryLRUAccessTokenCache} with a size of 100 to prevent unneeded 089 * requests to Box for access tokens. 090 * 091 * @param entityId enterprise ID or a user ID. 092 * @param entityType the type of entityId. 093 * @param clientID the client ID to use when exchanging the JWT assertion for an access token. 094 * @param clientSecret the client secret to use when exchanging the JWT assertion for an access token. 095 * @param encryptionPref the encryption preferences for signing the JWT. 096 */ 097 public BoxDeveloperEditionAPIConnection( 098 String entityId, 099 DeveloperEditionEntityType entityType, 100 String clientID, 101 String clientSecret, 102 JWTEncryptionPreferences encryptionPref 103 ) { 104 105 this( 106 entityId, 107 entityType, 108 clientID, 109 clientSecret, 110 encryptionPref, 111 new InMemoryLRUAccessTokenCache(DEFAULT_MAX_ENTRIES) 112 ); 113 } 114 115 /** 116 * Constructs a new BoxDeveloperEditionAPIConnection. 117 * 118 * @param entityId enterprise ID or a user ID. 119 * @param entityType the type of entityId. 120 * @param boxConfig box configuration settings object 121 * @param accessTokenCache the cache for storing access token information (to minimize fetching new tokens) 122 */ 123 public BoxDeveloperEditionAPIConnection(String entityId, DeveloperEditionEntityType entityType, 124 BoxConfig boxConfig, IAccessTokenCache accessTokenCache) { 125 126 this(entityId, entityType, boxConfig.getClientId(), boxConfig.getClientSecret(), 127 boxConfig.getJWTEncryptionPreferences(), accessTokenCache); 128 } 129 130 /** 131 * Creates a new Box Developer Edition connection with enterprise token leveraging an access token cache. 132 * 133 * @param enterpriseId the enterprise ID to use for requesting access token. 134 * @param clientId the client ID to use when exchanging the JWT assertion for an access token. 135 * @param clientSecret the client secret to use when exchanging the JWT assertion for an access token. 136 * @param encryptionPref the encryption preferences for signing the JWT. 137 * @param accessTokenCache the cache for storing access token information (to minimize fetching new tokens) 138 * @return a new instance of BoxAPIConnection. 139 */ 140 public static BoxDeveloperEditionAPIConnection getAppEnterpriseConnection( 141 String enterpriseId, 142 String clientId, 143 String clientSecret, 144 JWTEncryptionPreferences encryptionPref, 145 IAccessTokenCache accessTokenCache 146 ) { 147 148 BoxDeveloperEditionAPIConnection connection = new BoxDeveloperEditionAPIConnection(enterpriseId, 149 DeveloperEditionEntityType.ENTERPRISE, clientId, clientSecret, encryptionPref, accessTokenCache); 150 151 connection.tryRestoreUsingAccessTokenCache(); 152 153 return connection; 154 } 155 156 /** 157 * Creates a new Box Developer Edition connection with enterprise token. 158 * Uses {@link InMemoryLRUAccessTokenCache} with a size of 100 to prevent unneeded 159 * requests to Box for access tokens. 160 * 161 * @param enterpriseId the enterprise ID to use for requesting access token. 162 * @param clientId the client ID to use when exchanging the JWT assertion for an access token. 163 * @param clientSecret the client secret to use when exchanging the JWT assertion for an access token. 164 * @param encryptionPref the encryption preferences for signing the JWT. 165 * @return a new instance of BoxAPIConnection. 166 */ 167 public static BoxDeveloperEditionAPIConnection getAppEnterpriseConnection( 168 String enterpriseId, 169 String clientId, 170 String clientSecret, 171 JWTEncryptionPreferences encryptionPref 172 ) { 173 174 BoxDeveloperEditionAPIConnection connection = new BoxDeveloperEditionAPIConnection( 175 enterpriseId, 176 DeveloperEditionEntityType.ENTERPRISE, 177 clientId, 178 clientSecret, 179 encryptionPref 180 ); 181 182 connection.authenticate(); 183 184 return connection; 185 } 186 187 /** 188 * Creates a new Box Developer Edition connection with enterprise token leveraging BoxConfig and access token cache. 189 * 190 * @param boxConfig box configuration settings object 191 * @param accessTokenCache the cache for storing access token information (to minimize fetching new tokens) 192 * @return a new instance of BoxAPIConnection. 193 */ 194 public static BoxDeveloperEditionAPIConnection getAppEnterpriseConnection(BoxConfig boxConfig, 195 IAccessTokenCache accessTokenCache) { 196 197 return getAppEnterpriseConnection( 198 boxConfig.getEnterpriseId(), 199 boxConfig.getClientId(), 200 boxConfig.getClientSecret(), 201 boxConfig.getJWTEncryptionPreferences(), 202 accessTokenCache 203 ); 204 } 205 206 /** 207 * Creates a new Box Developer Edition connection with enterprise token leveraging BoxConfig. 208 * Uses {@link InMemoryLRUAccessTokenCache} with a size of 100 to prevent unneeded 209 * requests to Box for access tokens. 210 * 211 * @param boxConfig box configuration settings object 212 * @return a new instance of BoxAPIConnection. 213 */ 214 public static BoxDeveloperEditionAPIConnection getAppEnterpriseConnection(BoxConfig boxConfig) { 215 216 return getAppEnterpriseConnection( 217 boxConfig.getEnterpriseId(), 218 boxConfig.getClientId(), 219 boxConfig.getClientSecret(), 220 boxConfig.getJWTEncryptionPreferences() 221 ); 222 } 223 224 /** 225 * Creates a new Box Developer Edition connection with App User or Managed User token. 226 * 227 * @param userId the user ID to use for an App User. 228 * @param clientId the client ID to use when exchanging the JWT assertion for an access token. 229 * @param clientSecret the client secret to use when exchanging the JWT assertion for an access token. 230 * @param encryptionPref the encryption preferences for signing the JWT. 231 * @param accessTokenCache the cache for storing access token information (to minimize fetching new tokens) 232 * @return a new instance of BoxAPIConnection. 233 */ 234 public static BoxDeveloperEditionAPIConnection getUserConnection( 235 String userId, 236 String clientId, 237 String clientSecret, 238 JWTEncryptionPreferences encryptionPref, 239 IAccessTokenCache accessTokenCache 240 ) { 241 BoxDeveloperEditionAPIConnection connection = new BoxDeveloperEditionAPIConnection( 242 userId, 243 DeveloperEditionEntityType.USER, 244 clientId, 245 clientSecret, 246 encryptionPref, 247 accessTokenCache 248 ); 249 250 connection.tryRestoreUsingAccessTokenCache(); 251 252 return connection; 253 } 254 255 /** 256 * Creates a new Box Developer Edition connection with App User or Managed User token leveraging BoxConfig 257 * and access token cache. 258 * 259 * @param userId the user ID to use for an App User. 260 * @param boxConfig box configuration settings object 261 * @param accessTokenCache the cache for storing access token information (to minimize fetching new tokens) 262 * @return a new instance of BoxAPIConnection. 263 */ 264 public static BoxDeveloperEditionAPIConnection getUserConnection( 265 String userId, 266 BoxConfig boxConfig, 267 IAccessTokenCache accessTokenCache 268 ) { 269 return getUserConnection( 270 userId, 271 boxConfig.getClientId(), 272 boxConfig.getClientSecret(), 273 boxConfig.getJWTEncryptionPreferences(), 274 accessTokenCache 275 ); 276 } 277 278 /** 279 * Creates a new Box Developer Edition connection with App User or Managed User token. 280 * Uses {@link InMemoryLRUAccessTokenCache} with a size of 100 to prevent unneeded 281 * requests to Box for access tokens. 282 * 283 * @param userId the user ID to use for an App User. 284 * @param boxConfig box configuration settings object 285 * @return a new instance of BoxAPIConnection. 286 */ 287 public static BoxDeveloperEditionAPIConnection getUserConnection(String userId, BoxConfig boxConfig) { 288 return getUserConnection( 289 userId, 290 boxConfig.getClientId(), 291 boxConfig.getClientSecret(), 292 boxConfig.getJWTEncryptionPreferences(), 293 new InMemoryLRUAccessTokenCache(DEFAULT_MAX_ENTRIES)); 294 } 295 296 /** 297 * Disabling the non-Box Developer Edition authenticate method. 298 * 299 * @param authCode an auth code obtained from the first half of the OAuth process. 300 */ 301 public void authenticate(String authCode) { 302 throw new BoxAPIException("BoxDeveloperEditionAPIConnection does not allow authenticating with an auth code."); 303 } 304 305 /** 306 * Authenticates the API connection for Box Developer Edition. 307 */ 308 public void authenticate() { 309 URL url; 310 try { 311 url = new URL(this.getTokenURL()); 312 } catch (MalformedURLException e) { 313 assert false : "An invalid token URL indicates a bug in the SDK."; 314 throw new RuntimeException("An invalid token URL indicates a bug in the SDK.", e); 315 } 316 317 this.backoffCounter.reset(this.getMaxRetryAttempts() + 1); 318 NumericDate jwtTime = null; 319 String jwtAssertion; 320 String urlParameters; 321 BoxAPIRequest request; 322 String json = null; 323 final BoxLogger logger = BoxLogger.defaultLogger(); 324 325 while (this.backoffCounter.getAttemptsRemaining() > 0) { 326 // Reconstruct the JWT assertion, which regenerates the jti claim, with the new "current" time 327 jwtAssertion = this.constructJWTAssertion(jwtTime); 328 urlParameters = String.format(JWT_GRANT_TYPE, this.getClientID(), this.getClientSecret(), jwtAssertion); 329 330 request = new BoxAPIRequest(this, url, "POST"); 331 request.shouldAuthenticate(false); 332 request.setBody(urlParameters); 333 334 try (BoxJSONResponse response = (BoxJSONResponse) request.sendWithoutRetry()) { 335 // authentication uses form url encoded but response is JSON 336 json = response.getJSON(); 337 break; 338 } catch (BoxAPIException apiException) { 339 long responseReceivedTime = System.currentTimeMillis(); 340 341 if (!this.backoffCounter.decrement() 342 || (!BoxAPIRequest.isRequestRetryable(apiException) && !isResponseRetryable(apiException))) { 343 throw apiException; 344 } 345 346 logger.warn(String.format( 347 "Retrying authentication request due to transient error status=%d body=%s", 348 apiException.getResponseCode(), 349 apiException.getResponse() 350 )); 351 352 try { 353 List<String> retryAfterHeader = apiException.getHeaders().get("Retry-After"); 354 if (retryAfterHeader == null) { 355 this.backoffCounter.waitBackoff(); 356 } else { 357 int retryAfterDelay = Integer.parseInt(retryAfterHeader.get(0)) * 1000; 358 this.backoffCounter.waitBackoff(retryAfterDelay); 359 } 360 } catch (InterruptedException interruptedException) { 361 Thread.currentThread().interrupt(); 362 throw apiException; 363 } 364 365 long endWaitTime = System.currentTimeMillis(); 366 long secondsSinceResponseReceived = (endWaitTime - responseReceivedTime) / 1000; 367 368 try { 369 // Use the Date advertised by the Box server in the exception 370 // as the current time to synchronize clocks 371 jwtTime = this.getDateForJWTConstruction(apiException, secondsSinceResponseReceived); 372 } catch (Exception e) { 373 throw apiException; 374 } 375 376 } 377 } 378 379 if (json == null) { 380 throw new RuntimeException("Unable to read authentication response in SDK."); 381 } 382 383 JsonObject jsonObject = Json.parse(json).asObject(); 384 this.setAccessToken(jsonObject.get("access_token").asString()); 385 this.setLastRefresh(System.currentTimeMillis()); 386 this.setExpires(jsonObject.get("expires_in").asLong() * 1000); 387 388 //if token cache is specified, save to cache 389 if (this.accessTokenCache != null) { 390 String key = this.getAccessTokenCacheKey(); 391 JsonObject accessTokenCacheInfo = new JsonObject() 392 .add("accessToken", this.getAccessToken()) 393 .add("lastRefresh", this.getLastRefresh()) 394 .add("expires", this.getExpires()); 395 396 this.accessTokenCache.put(key, accessTokenCacheInfo.toString()); 397 } 398 } 399 400 private boolean isResponseRetryable(BoxAPIException apiException) { 401 return BoxAPIRequest.isResponseRetryable(apiException.getResponseCode(), apiException) 402 || isJtiNonUniqueError(apiException); 403 } 404 405 private boolean isJtiNonUniqueError(BoxAPIException apiException) { 406 return apiException.getResponseCode() == 400 407 && apiException.getResponse().contains("A unique 'jti' value is required"); 408 } 409 410 private NumericDate getDateForJWTConstruction(BoxAPIException apiException, long secondsSinceResponseDateReceived) { 411 NumericDate currentTime; 412 List<String> responseDates = apiException.getHeaders().get("Date"); 413 414 if (responseDates != null) { 415 String responseDate = responseDates.get(0); 416 SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss zzz"); 417 try { 418 Date date = dateFormat.parse(responseDate); 419 currentTime = NumericDate.fromMilliseconds(date.getTime()); 420 currentTime.addSeconds(secondsSinceResponseDateReceived); 421 } catch (ParseException e) { 422 currentTime = NumericDate.now(); 423 } 424 } else { 425 currentTime = NumericDate.now(); 426 } 427 return currentTime; 428 } 429 430 void setBackoffCounter(BackoffCounter counter) { 431 this.backoffCounter = counter; 432 } 433 434 /** 435 * BoxDeveloperEditionAPIConnection can always refresh, but this method is required elsewhere. 436 * 437 * @return true always. 438 */ 439 public boolean canRefresh() { 440 return true; 441 } 442 443 /** 444 * Refresh's this connection's access token using Box Developer Edition. 445 * 446 * @throws IllegalStateException if this connection's access token cannot be refreshed. 447 */ 448 public void refresh() { 449 this.getRefreshLock().writeLock().lock(); 450 451 try { 452 this.authenticate(); 453 } catch (BoxAPIException e) { 454 this.notifyError(e); 455 this.getRefreshLock().writeLock().unlock(); 456 throw e; 457 } 458 459 this.notifyRefresh(); 460 this.getRefreshLock().writeLock().unlock(); 461 } 462 463 private String getAccessTokenCacheKey() { 464 return String.format("/%s/%s/%s/%s", this.getUserAgent(), this.getClientID(), 465 this.entityType.toString(), this.entityID); 466 } 467 468 private void tryRestoreUsingAccessTokenCache() { 469 if (this.accessTokenCache == null) { 470 //no cache specified so force authentication 471 this.authenticate(); 472 } else { 473 String cachedTokenInfo = this.accessTokenCache.get(this.getAccessTokenCacheKey()); 474 if (cachedTokenInfo == null) { 475 //not found; probably first time for this client config so authenticate; info will then be cached 476 this.authenticate(); 477 } else { 478 //pull access token cache info; authentication will occur as needed (if token is expired) 479 JsonObject json = Json.parse(cachedTokenInfo).asObject(); 480 this.setAccessToken(json.get("accessToken").asString()); 481 this.setLastRefresh(json.get("lastRefresh").asLong()); 482 this.setExpires(json.get("expires").asLong()); 483 } 484 } 485 } 486 487 private String constructJWTAssertion(NumericDate now) { 488 JwtClaims claims = new JwtClaims(); 489 claims.setIssuer(this.getClientID()); 490 claims.setAudience(JWT_AUDIENCE); 491 if (now == null) { 492 claims.setExpirationTimeMinutesInTheFuture(0.5f); 493 } else { 494 now.addSeconds(30L); 495 claims.setExpirationTime(now); 496 } 497 claims.setSubject(this.entityID); 498 claims.setClaim("box_sub_type", this.entityType.toString()); 499 claims.setGeneratedJwtId(64); 500 501 JsonWebSignature jws = new JsonWebSignature(); 502 jws.setPayload(claims.toJson()); 503 jws.setKey(this.decryptPrivateKey()); 504 jws.setAlgorithmHeaderValue(this.getAlgorithmIdentifier()); 505 jws.setHeader("typ", "JWT"); 506 if ((this.publicKeyID != null) && !this.publicKeyID.isEmpty()) { 507 jws.setHeader("kid", this.publicKeyID); 508 } 509 510 String assertion; 511 512 try { 513 assertion = jws.getCompactSerialization(); 514 } catch (JoseException e) { 515 throw new BoxAPIException("Error serializing JSON Web Token assertion.", e); 516 } 517 518 return assertion; 519 } 520 521 private String getAlgorithmIdentifier() { 522 String algorithmId = AlgorithmIdentifiers.RSA_USING_SHA256; 523 switch (this.encryptionAlgorithm) { 524 case RSA_SHA_384: 525 algorithmId = AlgorithmIdentifiers.RSA_USING_SHA384; 526 break; 527 case RSA_SHA_512: 528 algorithmId = AlgorithmIdentifiers.RSA_USING_SHA512; 529 break; 530 case RSA_SHA_256: 531 default: 532 break; 533 } 534 535 return algorithmId; 536 } 537 538 private PrivateKey decryptPrivateKey() { 539 PrivateKey decryptedPrivateKey; 540 try { 541 PEMParser keyReader = new PEMParser(new StringReader(this.privateKey)); 542 Object keyPair = keyReader.readObject(); 543 keyReader.close(); 544 545 if (keyPair instanceof PrivateKeyInfo) { 546 PrivateKeyInfo keyInfo = (PrivateKeyInfo) keyPair; 547 decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo); 548 } else if (keyPair instanceof PEMEncryptedKeyPair) { 549 JcePEMDecryptorProviderBuilder builder = new JcePEMDecryptorProviderBuilder(); 550 PEMDecryptorProvider decryptionProvider = builder.build(this.privateKeyPassword.toCharArray()); 551 keyPair = ((PEMEncryptedKeyPair) keyPair).decryptKeyPair(decryptionProvider); 552 PrivateKeyInfo keyInfo = ((PEMKeyPair) keyPair).getPrivateKeyInfo(); 553 decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo); 554 } else if (keyPair instanceof PKCS8EncryptedPrivateKeyInfo) { 555 InputDecryptorProvider pkcs8Prov = new JceOpenSSLPKCS8DecryptorProviderBuilder().setProvider("BC") 556 .build(this.privateKeyPassword.toCharArray()); 557 PrivateKeyInfo keyInfo = ((PKCS8EncryptedPrivateKeyInfo) keyPair).decryptPrivateKeyInfo(pkcs8Prov); 558 decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo); 559 } else { 560 PrivateKeyInfo keyInfo = ((PEMKeyPair) keyPair).getPrivateKeyInfo(); 561 decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo); 562 } 563 } catch (IOException e) { 564 throw new BoxAPIException("Error parsing private key for Box Developer Edition.", e); 565 } catch (OperatorCreationException e) { 566 throw new BoxAPIException("Error parsing PKCS#8 private key for Box Developer Edition.", e); 567 } catch (PKCSException e) { 568 throw new BoxAPIException("Error parsing PKCS private key for Box Developer Edition.", e); 569 } 570 return decryptedPrivateKey; 571 } 572 573}