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