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
032    private final String entityID;
033    private final DeveloperEditionEntityType entityType;
034    private final EncryptionAlgorithm encryptionAlgorithm;
035    private final String publicKeyID;
036    private final String privateKey;
037    private final String privateKeyPassword;
038
039    /**
040     * Disabling an invalid constructor for Box Developer Edition.
041     * @param  accessToken  an initial access token to use for authenticating with the API.
042     */
043    public BoxDeveloperEditionAPIConnection(String accessToken) {
044        super(null);
045        throw new BoxAPIException("This constructor is not available for BoxDeveloperEditionAPIConnection.");
046    }
047
048    /**
049     * Disabling an invalid constructor for Box Developer Edition.
050     * @param  clientID     the client ID to use when refreshing the access token.
051     * @param  clientSecret the client secret to use when refreshing the access token.
052     * @param  accessToken  an initial access token to use for authenticating with the API.
053     * @param  refreshToken an initial refresh token to use when refreshing the access token.
054     */
055    public BoxDeveloperEditionAPIConnection(String clientID, String clientSecret, String accessToken,
056        String refreshToken) {
057
058        super(null);
059        throw new BoxAPIException("This constructor is not available for BoxDeveloperEditionAPIConnection.");
060    }
061
062    /**
063     * Disabling an invalid constructor for Box Developer Edition.
064     * @param  clientID     the client ID to use when exchanging the auth code for an access token.
065     * @param  clientSecret the client secret to use when exchanging the auth code for an access token.
066     * @param  authCode     an auth code obtained from the first half of the OAuth process.
067     */
068    public BoxDeveloperEditionAPIConnection(String clientID, String clientSecret, String authCode) {
069        super(null);
070        throw new BoxAPIException("This constructor is not available for BoxDeveloperEditionAPIConnection.");
071    }
072
073    /**
074     * Disabling an invalid constructor for Box Developer Edition.
075     * @param  clientID     the client ID to use when requesting an access token.
076     * @param  clientSecret the client secret to use when requesting an access token.
077     */
078    public BoxDeveloperEditionAPIConnection(String clientID, String clientSecret) {
079        super(null);
080        throw new BoxAPIException("This constructor is not available for BoxDeveloperEditionAPIConnection.");
081    }
082
083    /**
084     * Constructs a new BoxDeveloperEditionAPIConnection.
085     * @param entityId             enterprise ID or a user ID.
086     * @param entityType           the type of entityId.
087     * @param clientID             the client ID to use when exchanging the JWT assertion for an access token.
088     * @param clientSecret         the client secret to use when exchanging the JWT assertion for an access token.
089     * @param encryptionPref       the encryption preferences for signing the JWT.
090     */
091    public BoxDeveloperEditionAPIConnection(String entityId, DeveloperEditionEntityType entityType,
092        String clientID, String clientSecret, JWTEncryptionPreferences encryptionPref) {
093
094        super(clientID, clientSecret);
095
096        this.entityID = entityId;
097        this.entityType = entityType;
098        this.publicKeyID = encryptionPref.getPublicKeyID();
099        this.privateKey = encryptionPref.getPrivateKey();
100        this.privateKeyPassword = encryptionPref.getPrivateKeyPassword();
101        this.encryptionAlgorithm = encryptionPref.getEncryptionAlgorithm();
102    }
103
104    /**
105     * Creates a new Box Developer Edition connection with enterprise token.
106     * @param enterpriseId          the enterprise ID to use for requesting access token.
107     * @param clientId              the client ID to use when exchanging the JWT assertion for an access token.
108     * @param clientSecret          the client secret to use when exchanging the JWT assertion for an access token.
109     * @param encryptionPref        the encryption preferences for signing the JWT.
110     * @return a new instance of BoxAPIConnection.
111     */
112    public static BoxDeveloperEditionAPIConnection getAppEnterpriseConnection(String enterpriseId, String clientId,
113        String clientSecret, JWTEncryptionPreferences encryptionPref) {
114
115        BoxDeveloperEditionAPIConnection connection = new BoxDeveloperEditionAPIConnection(enterpriseId,
116            DeveloperEditionEntityType.ENTERPRISE, clientId, clientSecret, encryptionPref);
117
118        connection.authenticate();
119
120        return connection;
121    }
122
123    /**
124     * Creates a new Box Developer Edition connection with App User token.
125     * @param userId                the user ID to use for an App User.
126     * @param clientId              the client ID to use when exchanging the JWT assertion for an access token.
127     * @param clientSecret          the client secret to use when exchanging the JWT assertion for an access token.
128     * @param encryptionPref        the encryption preferences for signing the JWT.
129     * @return a new instance of BoxAPIConnection.
130     */
131    public static BoxDeveloperEditionAPIConnection getAppUserConnection(String userId, String clientId,
132        String clientSecret, JWTEncryptionPreferences encryptionPref) {
133
134        BoxDeveloperEditionAPIConnection connection = new BoxDeveloperEditionAPIConnection(userId,
135            DeveloperEditionEntityType.USER, clientId, clientSecret, encryptionPref);
136
137        connection.authenticate();
138
139        return connection;
140    }
141
142    /**
143     * Disabling the non-Box Developer Edition authenticate method.
144     * @param authCode an auth code obtained from the first half of the OAuth process.
145     */
146    public void authenticate(String authCode) {
147        throw new BoxAPIException("BoxDeveloperEditionAPIConnection does not allow authenticating with an auth code.");
148    }
149
150    /**
151     * Authenticates the API connection for Box Developer Edition.
152     */
153    public void authenticate() {
154        URL url = null;
155        try {
156            url = new URL(this.getTokenURL());
157        } catch (MalformedURLException e) {
158            assert false : "An invalid token URL indicates a bug in the SDK.";
159            throw new RuntimeException("An invalid token URL indicates a bug in the SDK.", e);
160        }
161
162        String jwtAssertion = this.constructJWTAssertion();
163
164        String urlParameters = String.format("grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer"
165                                           + "&client_id=%s&client_secret=%s&assertion=%s",
166            this.getClientID(), this.getClientSecret(), jwtAssertion);
167
168        BoxAPIRequest request = new BoxAPIRequest(this, url, "POST");
169        request.shouldAuthenticate(false);
170        request.setBody(urlParameters);
171
172        BoxJSONResponse response = (BoxJSONResponse) request.send();
173        String json = response.getJSON();
174
175        JsonObject jsonObject = JsonObject.readFrom(json);
176        this.setAccessToken(jsonObject.get("access_token").asString());
177        this.setLastRefresh(System.currentTimeMillis());
178        this.setExpires(jsonObject.get("expires_in").asLong() * 1000);
179    }
180
181    /**
182     * BoxDeveloperEditionAPIConnection can always refresh, but this method is required elsewhere.
183     * @return true always.
184     */
185    public boolean canRefresh() {
186        return true;
187    }
188
189    /**
190     * Refresh's this connection's access token using Box Developer Edition.
191     * @throws IllegalStateException if this connection's access token cannot be refreshed.
192     */
193    public void refresh() {
194        this.getRefreshLock().writeLock().lock();
195
196        try {
197            this.authenticate();
198        } catch (BoxAPIException e) {
199            this.notifyError(e);
200            this.getRefreshLock().writeLock().unlock();
201            throw e;
202        }
203
204        this.notifyRefresh();
205        this.getRefreshLock().writeLock().unlock();
206    }
207
208    private String constructJWTAssertion() {
209        JwtClaims claims = new JwtClaims();
210        claims.setIssuer(this.getClientID());
211        claims.setAudience(JWT_AUDIENCE);
212        claims.setExpirationTimeMinutesInTheFuture(1.0f);
213        claims.setSubject(this.entityID);
214        claims.setClaim("box_sub_type", this.entityType.toString());
215        claims.setGeneratedJwtId(64);
216
217        JsonWebSignature jws = new JsonWebSignature();
218        jws.setPayload(claims.toJson());
219        jws.setKey(this.decryptPrivateKey());
220        jws.setAlgorithmHeaderValue(this.getAlgorithmIdentifier());
221        jws.setHeader("typ", "JWT");
222        if ((this.publicKeyID != null) && !this.publicKeyID.isEmpty()) {
223            jws.setHeader("kid", this.publicKeyID);
224        }
225
226        String assertion;
227
228        try {
229            assertion = jws.getCompactSerialization();
230        } catch (JoseException e) {
231            throw new BoxAPIException("Error serializing JSON Web Token assertion.", e);
232        }
233
234        return assertion;
235    }
236
237    private String getAlgorithmIdentifier() {
238        String algorithmId = AlgorithmIdentifiers.RSA_USING_SHA256;
239        switch (this.encryptionAlgorithm) {
240            case RSA_SHA_384:
241                algorithmId = AlgorithmIdentifiers.RSA_USING_SHA384;
242                break;
243            case RSA_SHA_512:
244                algorithmId = AlgorithmIdentifiers.RSA_USING_SHA512;
245                break;
246            case RSA_SHA_256:
247            default:
248                break;
249        }
250
251        return algorithmId;
252    }
253
254    private PrivateKey decryptPrivateKey() {
255        PrivateKey decryptedPrivateKey;
256
257        try {
258            PEMParser keyReader = new PEMParser(new StringReader(this.privateKey));
259            Object keyPair = keyReader.readObject();
260            keyReader.close();
261
262            if (keyPair instanceof PEMEncryptedKeyPair) {
263                JcePEMDecryptorProviderBuilder builder = new JcePEMDecryptorProviderBuilder();
264                PEMDecryptorProvider decryptionProvider = builder.build(this.privateKeyPassword.toCharArray());
265                keyPair = ((PEMEncryptedKeyPair) keyPair).decryptKeyPair(decryptionProvider);
266            }
267
268            PrivateKeyInfo keyInfo = ((PEMKeyPair) keyPair).getPrivateKeyInfo();
269            decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
270        } catch (IOException e) {
271            throw new BoxAPIException("Error parsing private key for Box Developer Edition.", e);
272        }
273
274        return decryptedPrivateKey;
275    }
276}