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