All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.box.sdk.BoxDeveloperEditionAPIConnection Maven / Gradle / Ivy

There is a newer version: 4.12.0
Show newest version
package com.box.sdk;

import com.eclipsesource.json.Json;
import com.eclipsesource.json.JsonObject;
import java.io.IOException;
import java.io.StringReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.PrivateKey;
import java.security.Security;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMDecryptorProvider;
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
import org.bouncycastle.operator.InputDecryptorProvider;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
import org.bouncycastle.pkcs.PKCSException;
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.jwt.JwtClaims;
import org.jose4j.jwt.NumericDate;
import org.jose4j.lang.JoseException;

/**
 * Represents an authenticated Box Developer Edition connection to the Box API.
 *
 * 

This class handles everything for Box Developer Edition that isn't already handled by BoxAPIConnection.

*/ public class BoxDeveloperEditionAPIConnection extends BoxAPIConnection { private static final String JWT_AUDIENCE = "https://api.box.com/oauth2/token"; private static final String JWT_GRANT_TYPE = "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&client_id=%s&client_secret=%s&assertion=%s"; private static final int DEFAULT_MAX_ENTRIES = 100; static { Security.addProvider(new BouncyCastleProvider()); } private final String entityID; private final DeveloperEditionEntityType entityType; private final EncryptionAlgorithm encryptionAlgorithm; private final String publicKeyID; private final String privateKey; private final String privateKeyPassword; private BackoffCounter backoffCounter; private final IAccessTokenCache accessTokenCache; /** * Constructs a new BoxDeveloperEditionAPIConnection leveraging an access token cache. * * @param entityId enterprise ID or a user ID. * @param entityType the type of entityId. * @param clientID the client ID to use when exchanging the JWT assertion for an access token. * @param clientSecret the client secret to use when exchanging the JWT assertion for an access token. * @param encryptionPref the encryption preferences for signing the JWT. * @param accessTokenCache the cache for storing access token information (to minimize fetching new tokens) */ public BoxDeveloperEditionAPIConnection(String entityId, DeveloperEditionEntityType entityType, String clientID, String clientSecret, JWTEncryptionPreferences encryptionPref, IAccessTokenCache accessTokenCache) { super(clientID, clientSecret); this.entityID = entityId; this.entityType = entityType; this.publicKeyID = encryptionPref.getPublicKeyID(); this.privateKey = encryptionPref.getPrivateKey(); this.privateKeyPassword = encryptionPref.getPrivateKeyPassword(); this.encryptionAlgorithm = encryptionPref.getEncryptionAlgorithm(); this.accessTokenCache = accessTokenCache; this.backoffCounter = new BackoffCounter(new Time()); } /** * Constructs a new BoxDeveloperEditionAPIConnection. * Uses {@link InMemoryLRUAccessTokenCache} with a size of 100 to prevent unneeded * requests to Box for access tokens. * * @param entityId enterprise ID or a user ID. * @param entityType the type of entityId. * @param clientID the client ID to use when exchanging the JWT assertion for an access token. * @param clientSecret the client secret to use when exchanging the JWT assertion for an access token. * @param encryptionPref the encryption preferences for signing the JWT. */ public BoxDeveloperEditionAPIConnection( String entityId, DeveloperEditionEntityType entityType, String clientID, String clientSecret, JWTEncryptionPreferences encryptionPref ) { this( entityId, entityType, clientID, clientSecret, encryptionPref, new InMemoryLRUAccessTokenCache(DEFAULT_MAX_ENTRIES) ); } /** * Constructs a new BoxDeveloperEditionAPIConnection. * * @param entityId enterprise ID or a user ID. * @param entityType the type of entityId. * @param boxConfig box configuration settings object * @param accessTokenCache the cache for storing access token information (to minimize fetching new tokens) */ public BoxDeveloperEditionAPIConnection(String entityId, DeveloperEditionEntityType entityType, BoxConfig boxConfig, IAccessTokenCache accessTokenCache) { this(entityId, entityType, boxConfig.getClientId(), boxConfig.getClientSecret(), boxConfig.getJWTEncryptionPreferences(), accessTokenCache); } /** * Creates a new Box Developer Edition connection with enterprise token leveraging an access token cache. * * @param enterpriseId the enterprise ID to use for requesting access token. * @param clientId the client ID to use when exchanging the JWT assertion for an access token. * @param clientSecret the client secret to use when exchanging the JWT assertion for an access token. * @param encryptionPref the encryption preferences for signing the JWT. * @param accessTokenCache the cache for storing access token information (to minimize fetching new tokens) * @return a new instance of BoxAPIConnection. */ public static BoxDeveloperEditionAPIConnection getAppEnterpriseConnection( String enterpriseId, String clientId, String clientSecret, JWTEncryptionPreferences encryptionPref, IAccessTokenCache accessTokenCache ) { BoxDeveloperEditionAPIConnection connection = new BoxDeveloperEditionAPIConnection(enterpriseId, DeveloperEditionEntityType.ENTERPRISE, clientId, clientSecret, encryptionPref, accessTokenCache); connection.tryRestoreUsingAccessTokenCache(); return connection; } /** * Creates a new Box Developer Edition connection with enterprise token. * Uses {@link InMemoryLRUAccessTokenCache} with a size of 100 to prevent unneeded * requests to Box for access tokens. * * @param enterpriseId the enterprise ID to use for requesting access token. * @param clientId the client ID to use when exchanging the JWT assertion for an access token. * @param clientSecret the client secret to use when exchanging the JWT assertion for an access token. * @param encryptionPref the encryption preferences for signing the JWT. * @return a new instance of BoxAPIConnection. */ public static BoxDeveloperEditionAPIConnection getAppEnterpriseConnection( String enterpriseId, String clientId, String clientSecret, JWTEncryptionPreferences encryptionPref ) { BoxDeveloperEditionAPIConnection connection = new BoxDeveloperEditionAPIConnection( enterpriseId, DeveloperEditionEntityType.ENTERPRISE, clientId, clientSecret, encryptionPref ); connection.authenticate(); return connection; } /** * Creates a new Box Developer Edition connection with enterprise token leveraging BoxConfig and access token cache. * * @param boxConfig box configuration settings object * @param accessTokenCache the cache for storing access token information (to minimize fetching new tokens) * @return a new instance of BoxAPIConnection. */ public static BoxDeveloperEditionAPIConnection getAppEnterpriseConnection(BoxConfig boxConfig, IAccessTokenCache accessTokenCache) { return getAppEnterpriseConnection( boxConfig.getEnterpriseId(), boxConfig.getClientId(), boxConfig.getClientSecret(), boxConfig.getJWTEncryptionPreferences(), accessTokenCache ); } /** * Creates a new Box Developer Edition connection with enterprise token leveraging BoxConfig. * Uses {@link InMemoryLRUAccessTokenCache} with a size of 100 to prevent unneeded * requests to Box for access tokens. * * @param boxConfig box configuration settings object * @return a new instance of BoxAPIConnection. */ public static BoxDeveloperEditionAPIConnection getAppEnterpriseConnection(BoxConfig boxConfig) { return getAppEnterpriseConnection( boxConfig.getEnterpriseId(), boxConfig.getClientId(), boxConfig.getClientSecret(), boxConfig.getJWTEncryptionPreferences() ); } /** * Creates a new Box Developer Edition connection with App User or Managed User token. * * @param userId the user ID to use for an App User. * @param clientId the client ID to use when exchanging the JWT assertion for an access token. * @param clientSecret the client secret to use when exchanging the JWT assertion for an access token. * @param encryptionPref the encryption preferences for signing the JWT. * @param accessTokenCache the cache for storing access token information (to minimize fetching new tokens) * @return a new instance of BoxAPIConnection. */ public static BoxDeveloperEditionAPIConnection getUserConnection( String userId, String clientId, String clientSecret, JWTEncryptionPreferences encryptionPref, IAccessTokenCache accessTokenCache ) { BoxDeveloperEditionAPIConnection connection = new BoxDeveloperEditionAPIConnection( userId, DeveloperEditionEntityType.USER, clientId, clientSecret, encryptionPref, accessTokenCache ); connection.tryRestoreUsingAccessTokenCache(); return connection; } /** * Creates a new Box Developer Edition connection with App User or Managed User token leveraging BoxConfig * and access token cache. * * @param userId the user ID to use for an App User. * @param boxConfig box configuration settings object * @param accessTokenCache the cache for storing access token information (to minimize fetching new tokens) * @return a new instance of BoxAPIConnection. */ public static BoxDeveloperEditionAPIConnection getUserConnection( String userId, BoxConfig boxConfig, IAccessTokenCache accessTokenCache ) { return getUserConnection( userId, boxConfig.getClientId(), boxConfig.getClientSecret(), boxConfig.getJWTEncryptionPreferences(), accessTokenCache ); } /** * Creates a new Box Developer Edition connection with App User or Managed User token. * Uses {@link InMemoryLRUAccessTokenCache} with a size of 100 to prevent unneeded * requests to Box for access tokens. * * @param userId the user ID to use for an App User. * @param boxConfig box configuration settings object * @return a new instance of BoxAPIConnection. */ public static BoxDeveloperEditionAPIConnection getUserConnection(String userId, BoxConfig boxConfig) { return getUserConnection( userId, boxConfig.getClientId(), boxConfig.getClientSecret(), boxConfig.getJWTEncryptionPreferences(), new InMemoryLRUAccessTokenCache(DEFAULT_MAX_ENTRIES)); } /** * Disabling the non-Box Developer Edition authenticate method. * * @param authCode an auth code obtained from the first half of the OAuth process. */ public void authenticate(String authCode) { throw new BoxAPIException("BoxDeveloperEditionAPIConnection does not allow authenticating with an auth code."); } /** * Authenticates the API connection for Box Developer Edition. */ public void authenticate() { URL url; try { url = new URL(this.getTokenURL()); } catch (MalformedURLException e) { assert false : "An invalid token URL indicates a bug in the SDK."; throw new RuntimeException("An invalid token URL indicates a bug in the SDK.", e); } this.backoffCounter.reset(this.getMaxRetryAttempts() + 1); NumericDate jwtTime = null; String jwtAssertion; String urlParameters; BoxAPIRequest request; String json = null; final BoxLogger logger = BoxLogger.defaultLogger(); while (this.backoffCounter.getAttemptsRemaining() > 0) { // Reconstruct the JWT assertion, which regenerates the jti claim, with the new "current" time jwtAssertion = this.constructJWTAssertion(jwtTime); urlParameters = String.format(JWT_GRANT_TYPE, this.getClientID(), this.getClientSecret(), jwtAssertion); request = new BoxAPIRequest(this, url, "POST"); request.shouldAuthenticate(false); request.setBody(urlParameters); try (BoxJSONResponse response = (BoxJSONResponse) request.sendWithoutRetry()) { // authentication uses form url encoded but response is JSON json = response.getJSON(); break; } catch (BoxAPIException apiException) { long responseReceivedTime = System.currentTimeMillis(); if (!this.backoffCounter.decrement() || (!BoxAPIRequest.isRequestRetryable(apiException) && !isResponseRetryable(apiException))) { throw apiException; } logger.warn(String.format( "Retrying authentication request due to transient error status=%d body=%s", apiException.getResponseCode(), apiException.getResponse() )); try { List retryAfterHeader = apiException.getHeaders().get("Retry-After"); if (retryAfterHeader == null) { this.backoffCounter.waitBackoff(); } else { int retryAfterDelay = Integer.parseInt(retryAfterHeader.get(0)) * 1000; this.backoffCounter.waitBackoff(retryAfterDelay); } } catch (InterruptedException interruptedException) { Thread.currentThread().interrupt(); throw apiException; } long endWaitTime = System.currentTimeMillis(); long secondsSinceResponseReceived = (endWaitTime - responseReceivedTime) / 1000; try { // Use the Date advertised by the Box server in the exception // as the current time to synchronize clocks jwtTime = this.getDateForJWTConstruction(apiException, secondsSinceResponseReceived); } catch (Exception e) { throw apiException; } } } if (json == null) { throw new RuntimeException("Unable to read authentication response in SDK."); } JsonObject jsonObject = Json.parse(json).asObject(); this.setAccessToken(jsonObject.get("access_token").asString()); this.setLastRefresh(System.currentTimeMillis()); this.setExpires(jsonObject.get("expires_in").asLong() * 1000); //if token cache is specified, save to cache if (this.accessTokenCache != null) { String key = this.getAccessTokenCacheKey(); JsonObject accessTokenCacheInfo = new JsonObject() .add("accessToken", this.getAccessToken()) .add("lastRefresh", this.getLastRefresh()) .add("expires", this.getExpires()); this.accessTokenCache.put(key, accessTokenCacheInfo.toString()); } } private boolean isResponseRetryable(BoxAPIException apiException) { return BoxAPIRequest.isResponseRetryable(apiException.getResponseCode(), apiException) || isJtiNonUniqueError(apiException); } private boolean isJtiNonUniqueError(BoxAPIException apiException) { return apiException.getResponseCode() == 400 && apiException.getResponse().contains("A unique 'jti' value is required"); } private NumericDate getDateForJWTConstruction(BoxAPIException apiException, long secondsSinceResponseDateReceived) { NumericDate currentTime; List responseDates = apiException.getHeaders().get("Date"); if (responseDates != null) { String responseDate = responseDates.get(0); SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss zzz"); try { Date date = dateFormat.parse(responseDate); currentTime = NumericDate.fromMilliseconds(date.getTime()); currentTime.addSeconds(secondsSinceResponseDateReceived); } catch (ParseException e) { currentTime = NumericDate.now(); } } else { currentTime = NumericDate.now(); } return currentTime; } void setBackoffCounter(BackoffCounter counter) { this.backoffCounter = counter; } /** * BoxDeveloperEditionAPIConnection can always refresh, but this method is required elsewhere. * * @return true always. */ public boolean canRefresh() { return true; } /** * Refresh's this connection's access token using Box Developer Edition. * * @throws IllegalStateException if this connection's access token cannot be refreshed. */ public void refresh() { this.getRefreshLock().writeLock().lock(); try { this.authenticate(); } catch (BoxAPIException e) { this.notifyError(e); this.getRefreshLock().writeLock().unlock(); throw e; } this.notifyRefresh(); this.getRefreshLock().writeLock().unlock(); } private String getAccessTokenCacheKey() { return String.format("/%s/%s/%s/%s", this.getUserAgent(), this.getClientID(), this.entityType.toString(), this.entityID); } private void tryRestoreUsingAccessTokenCache() { if (this.accessTokenCache == null) { //no cache specified so force authentication this.authenticate(); } else { String cachedTokenInfo = this.accessTokenCache.get(this.getAccessTokenCacheKey()); if (cachedTokenInfo == null) { //not found; probably first time for this client config so authenticate; info will then be cached this.authenticate(); } else { //pull access token cache info; authentication will occur as needed (if token is expired) JsonObject json = Json.parse(cachedTokenInfo).asObject(); this.setAccessToken(json.get("accessToken").asString()); this.setLastRefresh(json.get("lastRefresh").asLong()); this.setExpires(json.get("expires").asLong()); } } } private String constructJWTAssertion(NumericDate now) { JwtClaims claims = new JwtClaims(); claims.setIssuer(this.getClientID()); claims.setAudience(JWT_AUDIENCE); if (now == null) { claims.setExpirationTimeMinutesInTheFuture(0.5f); } else { now.addSeconds(30L); claims.setExpirationTime(now); } claims.setSubject(this.entityID); claims.setClaim("box_sub_type", this.entityType.toString()); claims.setGeneratedJwtId(64); JsonWebSignature jws = new JsonWebSignature(); jws.setPayload(claims.toJson()); jws.setKey(this.decryptPrivateKey()); jws.setAlgorithmHeaderValue(this.getAlgorithmIdentifier()); jws.setHeader("typ", "JWT"); if ((this.publicKeyID != null) && !this.publicKeyID.isEmpty()) { jws.setHeader("kid", this.publicKeyID); } String assertion; try { assertion = jws.getCompactSerialization(); } catch (JoseException e) { throw new BoxAPIException("Error serializing JSON Web Token assertion.", e); } return assertion; } private String getAlgorithmIdentifier() { String algorithmId = AlgorithmIdentifiers.RSA_USING_SHA256; switch (this.encryptionAlgorithm) { case RSA_SHA_384: algorithmId = AlgorithmIdentifiers.RSA_USING_SHA384; break; case RSA_SHA_512: algorithmId = AlgorithmIdentifiers.RSA_USING_SHA512; break; case RSA_SHA_256: default: break; } return algorithmId; } private PrivateKey decryptPrivateKey() { PrivateKey decryptedPrivateKey; try { PEMParser keyReader = new PEMParser(new StringReader(this.privateKey)); Object keyPair = keyReader.readObject(); keyReader.close(); if (keyPair instanceof PrivateKeyInfo) { PrivateKeyInfo keyInfo = (PrivateKeyInfo) keyPair; decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo); } else if (keyPair instanceof PEMEncryptedKeyPair) { JcePEMDecryptorProviderBuilder builder = new JcePEMDecryptorProviderBuilder(); PEMDecryptorProvider decryptionProvider = builder.build(this.privateKeyPassword.toCharArray()); keyPair = ((PEMEncryptedKeyPair) keyPair).decryptKeyPair(decryptionProvider); PrivateKeyInfo keyInfo = ((PEMKeyPair) keyPair).getPrivateKeyInfo(); decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo); } else if (keyPair instanceof PKCS8EncryptedPrivateKeyInfo) { InputDecryptorProvider pkcs8Prov = new JceOpenSSLPKCS8DecryptorProviderBuilder().setProvider("BC") .build(this.privateKeyPassword.toCharArray()); PrivateKeyInfo keyInfo = ((PKCS8EncryptedPrivateKeyInfo) keyPair).decryptPrivateKeyInfo(pkcs8Prov); decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo); } else { PrivateKeyInfo keyInfo = ((PEMKeyPair) keyPair).getPrivateKeyInfo(); decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo); } } catch (IOException e) { throw new BoxAPIException("Error parsing private key for Box Developer Edition.", e); } catch (OperatorCreationException e) { throw new BoxAPIException("Error parsing PKCS#8 private key for Box Developer Edition.", e); } catch (PKCSException e) { throw new BoxAPIException("Error parsing PKCS private key for Box Developer Edition.", e); } return decryptedPrivateKey; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy