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