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