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