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}