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}