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     * Creates a new Box Developer Edition connection with enterprise token.
143     * @param enterpriseId          the enterprise ID to use for requesting access token.
144     * @param clientId              the client ID to use when exchanging the JWT assertion for an access token.
145     * @param clientSecret          the client secret to use when exchanging the JWT assertion for an access token.
146     * @param encryptionPref        the encryption preferences for signing the JWT.
147     * @return a new instance of BoxAPIConnection.
148     *
149     * @deprecated Use the version of this method that accepts an IAccessTokenCache to prevent unneeded
150     * requests to Box for access tokens.
151     */
152    @Deprecated
153    public static BoxDeveloperEditionAPIConnection getAppEnterpriseConnection(String enterpriseId, String clientId,
154        String clientSecret, JWTEncryptionPreferences encryptionPref) {
155
156        BoxDeveloperEditionAPIConnection connection = new BoxDeveloperEditionAPIConnection(enterpriseId,
157            DeveloperEditionEntityType.ENTERPRISE, clientId, clientSecret, encryptionPref);
158
159        connection.authenticate();
160
161        return connection;
162    }
163
164    /**
165     * Creates a new Box Developer Edition connection with enterprise token leveraging an access token cache.
166     * @param enterpriseId          the enterprise ID to use for requesting access token.
167     * @param clientId              the client ID to use when exchanging the JWT assertion for an access token.
168     * @param clientSecret          the client secret to use when exchanging the JWT assertion for an access token.
169     * @param encryptionPref        the encryption preferences for signing the JWT.
170     * @param accessTokenCache      the cache for storing access token information (to minimize fetching new tokens)
171     * @return a new instance of BoxAPIConnection.
172     */
173    public static BoxDeveloperEditionAPIConnection getAppEnterpriseConnection(String enterpriseId, String clientId,
174            String clientSecret, JWTEncryptionPreferences encryptionPref, IAccessTokenCache accessTokenCache) {
175
176        BoxDeveloperEditionAPIConnection connection = new BoxDeveloperEditionAPIConnection(enterpriseId,
177                DeveloperEditionEntityType.ENTERPRISE, clientId, clientSecret, encryptionPref, accessTokenCache);
178
179        connection.tryRestoreUsingAccessTokenCache();
180
181        return connection;
182    }
183
184    /**
185     * Creates a new Box Developer Edition connection with enterprise token leveraging BoxConfig.
186     * @param boxConfig             box configuration settings object
187     * @return a new instance of BoxAPIConnection.
188     */
189    public static BoxDeveloperEditionAPIConnection getAppEnterpriseConnection(BoxConfig boxConfig) {
190
191        BoxDeveloperEditionAPIConnection connection = getAppEnterpriseConnection(boxConfig.getEnterpriseId(),
192                boxConfig.getClientId(), boxConfig.getClientSecret(), boxConfig.getJWTEncryptionPreferences());
193
194        return connection;
195    }
196
197    /**
198     * Creates a new Box Developer Edition connection with enterprise token leveraging BoxConfig and access token cache.
199     * @param boxConfig             box configuration settings object
200     * @param accessTokenCache      the cache for storing access token information (to minimize fetching new tokens)
201     * @return a new instance of BoxAPIConnection.
202     */
203    public static BoxDeveloperEditionAPIConnection getAppEnterpriseConnection(BoxConfig boxConfig,
204                                                                              IAccessTokenCache accessTokenCache) {
205
206        BoxDeveloperEditionAPIConnection connection = getAppEnterpriseConnection(boxConfig.getEnterpriseId(),
207                boxConfig.getClientId(), boxConfig.getClientSecret(), boxConfig.getJWTEncryptionPreferences(),
208                accessTokenCache);
209
210        return connection;
211    }
212
213    /**
214     * Creates a new Box Developer Edition connection with App User token.
215     * @param userId                the user ID to use for an App User.
216     * @param clientId              the client ID to use when exchanging the JWT assertion for an access token.
217     * @param clientSecret          the client secret to use when exchanging the JWT assertion for an access token.
218     * @param encryptionPref        the encryption preferences for signing the JWT.
219     * @return a new instance of BoxAPIConnection.
220     *
221     * @deprecated Use the version of this method that accepts an IAccessTokenCache to prevent unneeded
222     * requests to Box for access tokens.
223     */
224    @Deprecated
225    public static BoxDeveloperEditionAPIConnection getAppUserConnection(String userId, String clientId,
226        String clientSecret, JWTEncryptionPreferences encryptionPref) {
227
228        BoxDeveloperEditionAPIConnection connection = new BoxDeveloperEditionAPIConnection(userId,
229            DeveloperEditionEntityType.USER, clientId, clientSecret, encryptionPref);
230
231        connection.authenticate();
232
233        return connection;
234    }
235
236    /**
237     * Creates a new Box Developer Edition connection with App User token.
238     * @param userId                the user ID to use for an App User.
239     * @param clientId              the client ID to use when exchanging the JWT assertion for an access token.
240     * @param clientSecret          the client secret to use when exchanging the JWT assertion for an access token.
241     * @param encryptionPref        the encryption preferences for signing the JWT.
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 getAppUserConnection(String userId, String clientId,
246        String clientSecret, JWTEncryptionPreferences encryptionPref, IAccessTokenCache accessTokenCache) {
247
248        BoxDeveloperEditionAPIConnection connection = new BoxDeveloperEditionAPIConnection(userId,
249                DeveloperEditionEntityType.USER, clientId, clientSecret, encryptionPref, accessTokenCache);
250
251        connection.tryRestoreUsingAccessTokenCache();
252
253        return connection;
254    }
255
256    /**
257     * Creates a new Box Developer Edition connection with App User token levaraging BoxConfig.
258     * @param userId                the user ID to use for an App User.
259     * @param boxConfig             box configuration settings object
260     * @return a new instance of BoxAPIConnection.
261     */
262    public static BoxDeveloperEditionAPIConnection getAppUserConnection(String userId, BoxConfig boxConfig) {
263        return getAppUserConnection(userId, boxConfig.getClientId(), boxConfig.getClientSecret(),
264                boxConfig.getJWTEncryptionPreferences());
265    }
266
267    /**
268     * Creates a new Box Developer Edition connection with App User token leveraging BoxConfig and access token cache.
269     * @param userId                the user ID to use for an App User.
270     * @param boxConfig             box configuration settings object
271     * @param accessTokenCache      the cache for storing access token information (to minimize fetching new tokens)
272     * @return a new instance of BoxAPIConnection.
273     */
274    public static BoxDeveloperEditionAPIConnection getAppUserConnection(String userId, BoxConfig boxConfig,
275                                                                        IAccessTokenCache accessTokenCache) {
276        return getAppUserConnection(userId, boxConfig.getClientId(), boxConfig.getClientSecret(),
277                boxConfig.getJWTEncryptionPreferences(), accessTokenCache);
278    }
279
280    /**
281     * Disabling the non-Box Developer Edition authenticate method.
282     * @param authCode an auth code obtained from the first half of the OAuth process.
283     */
284    public void authenticate(String authCode) {
285        throw new BoxAPIException("BoxDeveloperEditionAPIConnection does not allow authenticating with an auth code.");
286    }
287
288    /**
289     * Authenticates the API connection for Box Developer Edition.
290     */
291    public void authenticate() {
292        URL url;
293        try {
294            url = new URL(this.getTokenURL());
295        } catch (MalformedURLException e) {
296            assert false : "An invalid token URL indicates a bug in the SDK.";
297            throw new RuntimeException("An invalid token URL indicates a bug in the SDK.", e);
298        }
299
300        String jwtAssertion = this.constructJWTAssertion();
301
302        String urlParameters = String.format(JWT_GRANT_TYPE, this.getClientID(), this.getClientSecret(), jwtAssertion);
303
304        BoxAPIRequest request = new BoxAPIRequest(this, url, "POST");
305        request.shouldAuthenticate(false);
306        request.setBody(urlParameters);
307
308        BoxJSONResponse response = (BoxJSONResponse) request.send();
309        String json = response.getJSON();
310
311        JsonObject jsonObject = JsonObject.readFrom(json);
312        this.setAccessToken(jsonObject.get("access_token").asString());
313        this.setLastRefresh(System.currentTimeMillis());
314        this.setExpires(jsonObject.get("expires_in").asLong() * 1000);
315
316        //if token cache is specified, save to cache
317        if (this.accessTokenCache != null) {
318            String key = this.getAccessTokenCacheKey();
319            JsonObject accessTokenCacheInfo = new JsonObject()
320                    .add("accessToken", this.getAccessToken())
321                    .add("lastRefresh", this.getLastRefresh())
322                    .add("expires", this.getExpires());
323
324            this.accessTokenCache.put(key, accessTokenCacheInfo.toString());
325        }
326    }
327
328    /**
329     * BoxDeveloperEditionAPIConnection can always refresh, but this method is required elsewhere.
330     * @return true always.
331     */
332    public boolean canRefresh() {
333        return true;
334    }
335
336    /**
337     * Refresh's this connection's access token using Box Developer Edition.
338     * @throws IllegalStateException if this connection's access token cannot be refreshed.
339     */
340    public void refresh() {
341        this.getRefreshLock().writeLock().lock();
342
343        try {
344            this.authenticate();
345        } catch (BoxAPIException e) {
346            this.notifyError(e);
347            this.getRefreshLock().writeLock().unlock();
348            throw e;
349        }
350
351        this.notifyRefresh();
352        this.getRefreshLock().writeLock().unlock();
353    }
354
355    private String getAccessTokenCacheKey() {
356        return String.format("/%s/%s/%s/%s", this.getUserAgent(), this.getClientID(),
357                this.entityType.toString(), this.entityID);
358    }
359
360    private void tryRestoreUsingAccessTokenCache() {
361        if (this.accessTokenCache == null) {
362            //no cache specified so force authentication
363            this.authenticate();
364        } else {
365            String cachedTokenInfo = this.accessTokenCache.get(this.getAccessTokenCacheKey());
366            if (cachedTokenInfo == null) {
367                //not found; probably first time for this client config so authenticate; info will then be cached
368                this.authenticate();
369            } else {
370                //pull access token cache info; authentication will occur as needed (if token is expired)
371                JsonObject json = JsonObject.readFrom(cachedTokenInfo);
372                this.setAccessToken(json.get("accessToken").asString());
373                this.setLastRefresh(json.get("lastRefresh").asLong());
374                this.setExpires(json.get("expires").asLong());
375            }
376        }
377    }
378
379    private String constructJWTAssertion() {
380        JwtClaims claims = new JwtClaims();
381        claims.setIssuer(this.getClientID());
382        claims.setAudience(JWT_AUDIENCE);
383        claims.setExpirationTimeMinutesInTheFuture(1.0f);
384        claims.setSubject(this.entityID);
385        claims.setClaim("box_sub_type", this.entityType.toString());
386        claims.setGeneratedJwtId(64);
387
388        JsonWebSignature jws = new JsonWebSignature();
389        jws.setPayload(claims.toJson());
390        jws.setKey(this.decryptPrivateKey());
391        jws.setAlgorithmHeaderValue(this.getAlgorithmIdentifier());
392        jws.setHeader("typ", "JWT");
393        if ((this.publicKeyID != null) && !this.publicKeyID.isEmpty()) {
394            jws.setHeader("kid", this.publicKeyID);
395        }
396
397        String assertion;
398
399        try {
400            assertion = jws.getCompactSerialization();
401        } catch (JoseException e) {
402            throw new BoxAPIException("Error serializing JSON Web Token assertion.", e);
403        }
404
405        return assertion;
406    }
407
408    private String getAlgorithmIdentifier() {
409        String algorithmId = AlgorithmIdentifiers.RSA_USING_SHA256;
410        switch (this.encryptionAlgorithm) {
411            case RSA_SHA_384:
412                algorithmId = AlgorithmIdentifiers.RSA_USING_SHA384;
413                break;
414            case RSA_SHA_512:
415                algorithmId = AlgorithmIdentifiers.RSA_USING_SHA512;
416                break;
417            case RSA_SHA_256:
418            default:
419                break;
420        }
421
422        return algorithmId;
423    }
424
425    private PrivateKey decryptPrivateKey() {
426        PrivateKey decryptedPrivateKey = null;
427        try {
428            PEMParser keyReader = new PEMParser(new StringReader(this.privateKey));
429            Object keyPair = keyReader.readObject();
430            keyReader.close();
431
432            if (keyPair instanceof PEMEncryptedKeyPair) {
433                JcePEMDecryptorProviderBuilder builder = new JcePEMDecryptorProviderBuilder();
434                PEMDecryptorProvider decryptionProvider = builder.build(this.privateKeyPassword.toCharArray());
435                keyPair = ((PEMEncryptedKeyPair) keyPair).decryptKeyPair(decryptionProvider);
436                PrivateKeyInfo keyInfo = ((PEMKeyPair) keyPair).getPrivateKeyInfo();
437                decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
438            } else if (keyPair instanceof PKCS8EncryptedPrivateKeyInfo) {
439                InputDecryptorProvider pkcs8Prov = new JceOpenSSLPKCS8DecryptorProviderBuilder().setProvider("BC")
440                    .build(this.privateKeyPassword.toCharArray());
441                PrivateKeyInfo keyInfo =  ((PKCS8EncryptedPrivateKeyInfo) keyPair).decryptPrivateKeyInfo(pkcs8Prov);
442                decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
443            } else {
444                PrivateKeyInfo keyInfo = ((PEMKeyPair) keyPair).getPrivateKeyInfo();
445                decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
446            }
447        } catch (IOException e) {
448            throw new BoxAPIException("Error parsing private key for Box Developer Edition.", e);
449        } catch (OperatorCreationException e) {
450            throw new BoxAPIException("Error parsing PKCS#8 private key for Box Developer Edition.", e);
451        } catch (PKCSException e) {
452            throw new BoxAPIException("Error parsing PKCS private key for Box Developer Edition.", e);
453        }
454        return decryptedPrivateKey;
455    }
456
457}