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