• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

box / box-java-sdk / #4105

20 Nov 2024 02:54AM CUT coverage: 71.761% (-0.002%) from 71.763%
#4105

Pull #1272

github

web-flow
Merge 1f3ddff9c into 5eb4c93bd
Pull Request #1272: feat: make `tryRestoreUsingAccessTokenCache` in Box API connection public

8048 of 11215 relevant lines covered (71.76%)

0.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

53.22
/src/main/java/com/box/sdk/BoxDeveloperEditionAPIConnection.java
1
package com.box.sdk;
2

3
import com.eclipsesource.json.Json;
4
import com.eclipsesource.json.JsonObject;
5
import java.net.MalformedURLException;
6
import java.net.URL;
7
import java.text.ParseException;
8
import java.text.SimpleDateFormat;
9
import java.util.Date;
10
import java.util.List;
11
import org.jose4j.jws.AlgorithmIdentifiers;
12
import org.jose4j.jws.JsonWebSignature;
13
import org.jose4j.jwt.JwtClaims;
14
import org.jose4j.jwt.NumericDate;
15
import org.jose4j.lang.JoseException;
16

17
/**
18
 * Represents an authenticated Box Developer Edition connection to the Box API.
19
 *
20
 * <p>This class handles everything for Box Developer Edition that isn't already handled by BoxAPIConnection.</p>
21
 */
22
public class BoxDeveloperEditionAPIConnection extends BoxAPIConnection {
1✔
23

24
    private static final String JWT_AUDIENCE = "https://api.box.com/oauth2/token";
25
    private static final String JWT_GRANT_TYPE =
26
        "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&client_id=%s&client_secret=%s&assertion=%s";
27
    private static final int DEFAULT_MAX_ENTRIES = 100;
28

29
    private final String entityID;
30
    private final DeveloperEditionEntityType entityType;
31
    private final EncryptionAlgorithm encryptionAlgorithm;
32
    private final String publicKeyID;
33
    private final String privateKey;
34
    private final String privateKeyPassword;
35
    private BackoffCounter backoffCounter;
36
    private final IAccessTokenCache accessTokenCache;
37
    private final IPrivateKeyDecryptor privateKeyDecryptor;
38

39
    /**
40
     * Constructs a new BoxDeveloperEditionAPIConnection leveraging an access token cache.
41
     *
42
     * @param entityId         enterprise ID or a user ID.
43
     * @param entityType       the type of entityId.
44
     * @param clientID         the client ID to use when exchanging the JWT assertion for an access token.
45
     * @param clientSecret     the client secret to use when exchanging the JWT assertion for an access token.
46
     * @param encryptionPref   the encryption preferences for signing the JWT.
47
     * @param accessTokenCache the cache for storing access token information (to minimize fetching new tokens)
48
     */
49
    public BoxDeveloperEditionAPIConnection(String entityId, DeveloperEditionEntityType entityType,
50
                                            String clientID, String clientSecret,
51
                                            JWTEncryptionPreferences encryptionPref,
52
                                            IAccessTokenCache accessTokenCache) {
53

54
        super(clientID, clientSecret);
1✔
55

56
        this.entityID = entityId;
1✔
57
        this.entityType = entityType;
1✔
58
        this.publicKeyID = encryptionPref.getPublicKeyID();
1✔
59
        this.privateKey = encryptionPref.getPrivateKey();
1✔
60
        this.privateKeyPassword = encryptionPref.getPrivateKeyPassword();
1✔
61
        this.encryptionAlgorithm = encryptionPref.getEncryptionAlgorithm();
1✔
62
        this.privateKeyDecryptor = encryptionPref.getPrivateKeyDecryptor();
1✔
63
        this.accessTokenCache = accessTokenCache;
1✔
64
        this.backoffCounter = new BackoffCounter(new Time());
1✔
65
    }
1✔
66

67
    /**
68
     * Constructs a new BoxDeveloperEditionAPIConnection.
69
     * Uses {@link InMemoryLRUAccessTokenCache} with a size of 100 to prevent unneeded
70
     * requests to Box for access tokens.
71
     *
72
     * @param entityId       enterprise ID or a user ID.
73
     * @param entityType     the type of entityId.
74
     * @param clientID       the client ID to use when exchanging the JWT assertion for an access token.
75
     * @param clientSecret   the client secret to use when exchanging the JWT assertion for an access token.
76
     * @param encryptionPref the encryption preferences for signing the JWT.
77
     */
78
    public BoxDeveloperEditionAPIConnection(
79
        String entityId,
80
        DeveloperEditionEntityType entityType,
81
        String clientID,
82
        String clientSecret,
83
        JWTEncryptionPreferences encryptionPref
84
    ) {
85

86
        this(
×
87
            entityId,
88
            entityType,
89
            clientID,
90
            clientSecret,
91
            encryptionPref,
92
            new InMemoryLRUAccessTokenCache(DEFAULT_MAX_ENTRIES)
93
        );
94
    }
×
95

96
    /**
97
     * Constructs a new BoxDeveloperEditionAPIConnection.
98
     *
99
     * @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());
1✔
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
        }
1✔
297

298
        this.backoffCounter.reset(this.getMaxRetryAttempts() + 1);
1✔
299
        NumericDate jwtTime = null;
1✔
300
        String jwtAssertion;
301
        String urlParameters;
302
        BoxAPIRequest request;
303
        String json = null;
1✔
304
        final BoxLogger logger = BoxLogger.defaultLogger();
1✔
305

306
        while (this.backoffCounter.getAttemptsRemaining() > 0) {
1✔
307
            // Reconstruct the JWT assertion, which regenerates the jti claim, with the new "current" time
308
            jwtAssertion = this.constructJWTAssertion(jwtTime);
1✔
309
            urlParameters = String.format(JWT_GRANT_TYPE, this.getClientID(), this.getClientSecret(), jwtAssertion);
1✔
310

311
            request = new BoxAPIRequest(this, url, "POST");
1✔
312
            request.shouldAuthenticate(false);
1✔
313
            request.setBody(urlParameters);
1✔
314

315
            try (BoxJSONResponse response = (BoxJSONResponse) request.sendWithoutRetry()) {
1✔
316
                // authentication uses form url encoded but response is JSON
317
                json = response.getJSON();
1✔
318
                break;
319
            } catch (BoxAPIException apiException) {
1✔
320
                long responseReceivedTime = System.currentTimeMillis();
1✔
321

322
                if (!this.backoffCounter.decrement()
1✔
323
                    || (!BoxAPIRequest.isRequestRetryable(apiException) && !isResponseRetryable(apiException))) {
1✔
324
                    throw apiException;
1✔
325
                }
326

327
                logger.warn(String.format(
1✔
328
                    "Retrying authentication request due to transient error status=%d body=%s",
329
                    apiException.getResponseCode(),
1✔
330
                    apiException.getResponse()
1✔
331
                ));
332

333
                try {
334
                    List<String> retryAfterHeader = apiException.getHeaders().get("Retry-After");
1✔
335
                    if (retryAfterHeader == null) {
1✔
336
                        this.backoffCounter.waitBackoff();
1✔
337
                    } else {
338
                        int retryAfterDelay = Integer.parseInt(retryAfterHeader.get(0)) * 1000;
1✔
339
                        this.backoffCounter.waitBackoff(retryAfterDelay);
1✔
340
                    }
341
                } catch (InterruptedException interruptedException) {
×
342
                    Thread.currentThread().interrupt();
×
343
                    throw apiException;
×
344
                }
1✔
345

346
                long endWaitTime = System.currentTimeMillis();
1✔
347
                long secondsSinceResponseReceived = (endWaitTime - responseReceivedTime) / 1000;
1✔
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);
1✔
353
                } catch (Exception e) {
×
354
                    throw apiException;
×
355
                }
1✔
356

357
            }
1✔
358
        }
359

360
        if (json == null) {
1✔
361
            throw new RuntimeException("Unable to read authentication response in SDK.");
×
362
        }
363

364
        JsonObject jsonObject = Json.parse(json).asObject();
1✔
365
        this.setAccessToken(jsonObject.get("access_token").asString());
1✔
366
        this.setLastRefresh(System.currentTimeMillis());
1✔
367
        this.setExpires(jsonObject.get("expires_in").asLong() * 1000);
1✔
368

369
        //if token cache is specified, save to cache
370
        if (this.accessTokenCache != null) {
1✔
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
    }
1✔
380

381
    private boolean isResponseRetryable(BoxAPIException apiException) {
382
        return BoxAPIRequest.isResponseRetryable(apiException.getResponseCode(), apiException)
1✔
383
            || isJtiNonUniqueError(apiException);
1✔
384
    }
385

386
    private boolean isJtiNonUniqueError(BoxAPIException apiException) {
387
        return apiException.getResponseCode() == 400
1✔
388
            && apiException.getResponse().contains("A unique 'jti' value is required");
1✔
389
    }
390

391
    private NumericDate getDateForJWTConstruction(BoxAPIException apiException, long secondsSinceResponseDateReceived) {
392
        NumericDate currentTime;
393
        List<String> responseDates = apiException.getHeaders().get("Date");
1✔
394

395
        if (responseDates != null) {
1✔
396
            String responseDate = responseDates.get(0);
1✔
397
            SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss zzz");
1✔
398
            try {
399
                Date date = dateFormat.parse(responseDate);
1✔
400
                currentTime = NumericDate.fromMilliseconds(date.getTime());
1✔
401
                currentTime.addSeconds(secondsSinceResponseDateReceived);
1✔
402
            } catch (ParseException e) {
×
403
                currentTime = NumericDate.now();
×
404
            }
1✔
405
        } else {
1✔
406
            currentTime = NumericDate.now();
1✔
407
        }
408
        return currentTime;
1✔
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;
1✔
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
    /**
450
     * Tries to restore the connection using the access token cache.
451
     */
452
    public void tryRestoreUsingAccessTokenCache() {
453
        if (this.accessTokenCache == null) {
×
454
            //no cache specified so force authentication
455
            this.authenticate();
×
456
        } else {
457
            String cachedTokenInfo = this.accessTokenCache.get(this.getAccessTokenCacheKey());
×
458
            if (cachedTokenInfo == null) {
×
459
                //not found; probably first time for this client config so authenticate; info will then be cached
460
                this.authenticate();
×
461
            } else {
462
                //pull access token cache info; authentication will occur as needed (if token is expired)
463
                JsonObject json = Json.parse(cachedTokenInfo).asObject();
×
464
                this.setAccessToken(json.get("accessToken").asString());
×
465
                this.setLastRefresh(json.get("lastRefresh").asLong());
×
466
                this.setExpires(json.get("expires").asLong());
×
467
            }
468
        }
469
    }
×
470

471
    private String constructJWTAssertion(NumericDate now) {
472
        JwtClaims claims = new JwtClaims();
1✔
473
        claims.setIssuer(this.getClientID());
1✔
474
        claims.setAudience(JWT_AUDIENCE);
1✔
475
        if (now == null) {
1✔
476
            claims.setExpirationTimeMinutesInTheFuture(0.5f);
1✔
477
        } else {
478
            now.addSeconds(30L);
1✔
479
            claims.setExpirationTime(now);
1✔
480
        }
481
        claims.setSubject(this.entityID);
1✔
482
        claims.setClaim("box_sub_type", this.entityType.toString());
1✔
483
        claims.setGeneratedJwtId(64);
1✔
484

485
        JsonWebSignature jws = new JsonWebSignature();
1✔
486
        jws.setPayload(claims.toJson());
1✔
487
        jws.setKey(this.privateKeyDecryptor.decryptPrivateKey(this.privateKey, this.privateKeyPassword));
1✔
488
        jws.setAlgorithmHeaderValue(this.getAlgorithmIdentifier());
1✔
489
        jws.setHeader("typ", "JWT");
1✔
490
        if ((this.publicKeyID != null) && !this.publicKeyID.isEmpty()) {
1✔
491
            jws.setHeader("kid", this.publicKeyID);
1✔
492
        }
493

494
        String assertion;
495

496
        try {
497
            assertion = jws.getCompactSerialization();
1✔
498
        } catch (JoseException e) {
×
499
            throw new BoxAPIException("Error serializing JSON Web Token assertion.", e);
×
500
        }
1✔
501

502
        return assertion;
1✔
503
    }
504

505
    private String getAlgorithmIdentifier() {
506
        String algorithmId = AlgorithmIdentifiers.RSA_USING_SHA256;
1✔
507
        switch (this.encryptionAlgorithm) {
1✔
508
            case RSA_SHA_384:
509
                algorithmId = AlgorithmIdentifiers.RSA_USING_SHA384;
×
510
                break;
×
511
            case RSA_SHA_512:
512
                algorithmId = AlgorithmIdentifiers.RSA_USING_SHA512;
×
513
                break;
×
514
            case RSA_SHA_256:
515
            default:
516
                break;
517
        }
518

519
        return algorithmId;
1✔
520
    }
521
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc