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}