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; 009import java.text.ParseException; 010import java.text.SimpleDateFormat; 011import java.util.Date; 012import java.util.List; 013 014import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; 015import org.bouncycastle.jce.provider.BouncyCastleProvider; 016import org.bouncycastle.openssl.PEMDecryptorProvider; 017import org.bouncycastle.openssl.PEMEncryptedKeyPair; 018import org.bouncycastle.openssl.PEMKeyPair; 019import org.bouncycastle.openssl.PEMParser; 020import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; 021import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder; 022import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder; 023import org.bouncycastle.operator.InputDecryptorProvider; 024import org.bouncycastle.operator.OperatorCreationException; 025import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo; 026import org.bouncycastle.pkcs.PKCSException; 027import org.jose4j.jws.AlgorithmIdentifiers; 028import org.jose4j.jws.JsonWebSignature; 029import org.jose4j.jwt.JwtClaims; 030import org.jose4j.jwt.NumericDate; 031import org.jose4j.lang.JoseException; 032 033import com.eclipsesource.json.JsonObject; 034 035/** 036 * Represents an authenticated Box Developer Edition connection to the Box API. 037 * 038 * <p>This class handles everything for Box Developer Edition that isn't already handled by BoxAPIConnection.</p> 039 */ 040public class BoxDeveloperEditionAPIConnection extends BoxAPIConnection { 041 042 private static final String JWT_AUDIENCE = "https://api.box.com/oauth2/token"; 043 private static final String JWT_GRANT_TYPE = 044 "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&client_id=%s&client_secret=%s&assertion=%s"; 045 046 private final String entityID; 047 private final DeveloperEditionEntityType entityType; 048 private final EncryptionAlgorithm encryptionAlgorithm; 049 private final String publicKeyID; 050 private final String privateKey; 051 private final String privateKeyPassword; 052 053 private IAccessTokenCache accessTokenCache; 054 055 static { 056 Security.addProvider(new BouncyCastleProvider()); 057 } 058 059 /** 060 * Disabling an invalid constructor for Box Developer Edition. 061 * @param accessToken an initial access token to use for authenticating with the API. 062 */ 063 private BoxDeveloperEditionAPIConnection(String accessToken) { 064 super(accessToken); 065 throw new BoxAPIException("This constructor is not available for BoxDeveloperEditionAPIConnection."); 066 } 067 068 /** 069 * Disabling an invalid constructor for Box Developer Edition. 070 * @param clientID the client ID to use when refreshing the access token. 071 * @param clientSecret the client secret to use when refreshing the access token. 072 * @param accessToken an initial access token to use for authenticating with the API. 073 * @param refreshToken an initial refresh token to use when refreshing the access token. 074 */ 075 private BoxDeveloperEditionAPIConnection(String clientID, String clientSecret, String accessToken, 076 String refreshToken) { 077 super(accessToken); 078 throw new BoxAPIException("This constructor is not available for BoxDeveloperEditionAPIConnection."); 079 } 080 081 /** 082 * Disabling an invalid constructor for Box Developer Edition. 083 * @param clientID the client ID to use when exchanging the auth code for an access token. 084 * @param clientSecret the client secret to use when exchanging the auth code for an access token. 085 * @param authCode an auth code obtained from the first half of the OAuth process. 086 */ 087 private BoxDeveloperEditionAPIConnection(String clientID, String clientSecret, String authCode) { 088 super(clientID, clientSecret, authCode); 089 throw new BoxAPIException("This constructor is not available for BoxDeveloperEditionAPIConnection."); 090 } 091 092 /** 093 * Disabling an invalid constructor for Box Developer Edition. 094 * @param clientID the client ID to use when requesting an access token. 095 * @param clientSecret the client secret to use when requesting an access token. 096 */ 097 private BoxDeveloperEditionAPIConnection(String clientID, String clientSecret) { 098 super(clientID, clientSecret); 099 throw new BoxAPIException("This constructor is not available for BoxDeveloperEditionAPIConnection."); 100 } 101 102 /** 103 * Constructs a new BoxDeveloperEditionAPIConnection. 104 * @param entityId enterprise ID or a user ID. 105 * @param entityType the type of entityId. 106 * @param clientID the client ID to use when exchanging the JWT assertion for an access token. 107 * @param clientSecret the client secret to use when exchanging the JWT assertion for an access token. 108 * @param encryptionPref the encryption preferences for signing the JWT. 109 * 110 * @deprecated Use the version of this constructor that accepts an IAccessTokenCache to prevent unneeded 111 * requests to Box for access tokens. 112 */ 113 @Deprecated 114 public BoxDeveloperEditionAPIConnection(String entityId, DeveloperEditionEntityType entityType, 115 String clientID, String clientSecret, JWTEncryptionPreferences encryptionPref) { 116 117 this(entityId, entityType, clientID, clientSecret, encryptionPref, null); 118 } 119 120 121 /** 122 * Constructs a new BoxDeveloperEditionAPIConnection leveraging an access token cache. 123 * @param entityId enterprise ID or a user ID. 124 * @param entityType the type of entityId. 125 * @param clientID the client ID to use when exchanging the JWT assertion for an access token. 126 * @param clientSecret the client secret to use when exchanging the JWT assertion for an access token. 127 * @param encryptionPref the encryption preferences for signing the JWT. 128 * @param accessTokenCache the cache for storing access token information (to minimize fetching new tokens) 129 */ 130 public BoxDeveloperEditionAPIConnection(String entityId, DeveloperEditionEntityType entityType, 131 String clientID, String clientSecret, 132 JWTEncryptionPreferences encryptionPref, 133 IAccessTokenCache accessTokenCache) { 134 135 super(clientID, clientSecret); 136 137 this.entityID = entityId; 138 this.entityType = entityType; 139 this.publicKeyID = encryptionPref.getPublicKeyID(); 140 this.privateKey = encryptionPref.getPrivateKey(); 141 this.privateKeyPassword = encryptionPref.getPrivateKeyPassword(); 142 this.encryptionAlgorithm = encryptionPref.getEncryptionAlgorithm(); 143 this.accessTokenCache = accessTokenCache; 144 } 145 146 /** 147 * Constructs a new BoxDeveloperEditionAPIConnection. 148 * @param entityId enterprise ID or a user ID. 149 * @param entityType the type of entityId. 150 * @param boxConfig box configuration settings object 151 * @param accessTokenCache the cache for storing access token information (to minimize fetching new tokens) 152 * 153 */ 154 public BoxDeveloperEditionAPIConnection(String entityId, DeveloperEditionEntityType entityType, 155 BoxConfig boxConfig, IAccessTokenCache accessTokenCache) { 156 157 this(entityId, entityType, boxConfig.getClientId(), boxConfig.getClientSecret(), 158 boxConfig.getJWTEncryptionPreferences(), accessTokenCache); 159 } 160 161 /** 162 * Creates a new Box Developer Edition connection with enterprise token. 163 * @param enterpriseId the enterprise ID to use for requesting access token. 164 * @param clientId the client ID to use when exchanging the JWT assertion for an access token. 165 * @param clientSecret the client secret to use when exchanging the JWT assertion for an access token. 166 * @param encryptionPref the encryption preferences for signing the JWT. 167 * @return a new instance of BoxAPIConnection. 168 * 169 * @deprecated Use the version of this method that accepts an IAccessTokenCache to prevent unneeded 170 * requests to Box for access tokens. 171 */ 172 @Deprecated 173 public static BoxDeveloperEditionAPIConnection getAppEnterpriseConnection(String enterpriseId, String clientId, 174 String clientSecret, JWTEncryptionPreferences encryptionPref) { 175 176 BoxDeveloperEditionAPIConnection connection = new BoxDeveloperEditionAPIConnection(enterpriseId, 177 DeveloperEditionEntityType.ENTERPRISE, clientId, clientSecret, encryptionPref); 178 179 connection.authenticate(); 180 181 return connection; 182 } 183 184 /** 185 * Creates a new Box Developer Edition connection with enterprise token leveraging an access token cache. 186 * @param enterpriseId the enterprise ID to use for requesting access token. 187 * @param clientId the client ID to use when exchanging the JWT assertion for an access token. 188 * @param clientSecret the client secret to use when exchanging the JWT assertion for an access token. 189 * @param encryptionPref the encryption preferences for signing the JWT. 190 * @param accessTokenCache the cache for storing access token information (to minimize fetching new tokens) 191 * @return a new instance of BoxAPIConnection. 192 */ 193 public static BoxDeveloperEditionAPIConnection getAppEnterpriseConnection(String enterpriseId, String clientId, 194 String clientSecret, JWTEncryptionPreferences encryptionPref, IAccessTokenCache accessTokenCache) { 195 196 BoxDeveloperEditionAPIConnection connection = new BoxDeveloperEditionAPIConnection(enterpriseId, 197 DeveloperEditionEntityType.ENTERPRISE, clientId, clientSecret, encryptionPref, accessTokenCache); 198 199 connection.tryRestoreUsingAccessTokenCache(); 200 201 return connection; 202 } 203 204 /** 205 * Creates a new Box Developer Edition connection with enterprise token leveraging BoxConfig. 206 * @param boxConfig box configuration settings object 207 * @return a new instance of BoxAPIConnection. 208 */ 209 public static BoxDeveloperEditionAPIConnection getAppEnterpriseConnection(BoxConfig boxConfig) { 210 211 BoxDeveloperEditionAPIConnection connection = getAppEnterpriseConnection(boxConfig.getEnterpriseId(), 212 boxConfig.getClientId(), boxConfig.getClientSecret(), boxConfig.getJWTEncryptionPreferences()); 213 214 return connection; 215 } 216 217 /** 218 * Creates a new Box Developer Edition connection with enterprise token leveraging BoxConfig and access token cache. 219 * @param boxConfig box configuration settings object 220 * @param accessTokenCache the cache for storing access token information (to minimize fetching new tokens) 221 * @return a new instance of BoxAPIConnection. 222 */ 223 public static BoxDeveloperEditionAPIConnection getAppEnterpriseConnection(BoxConfig boxConfig, 224 IAccessTokenCache accessTokenCache) { 225 226 BoxDeveloperEditionAPIConnection connection = getAppEnterpriseConnection(boxConfig.getEnterpriseId(), 227 boxConfig.getClientId(), boxConfig.getClientSecret(), boxConfig.getJWTEncryptionPreferences(), 228 accessTokenCache); 229 230 return connection; 231 } 232 233 /** 234 * Creates a new Box Developer Edition connection with App User token. 235 * @param userId the user ID to use for an App User. 236 * @param clientId the client ID to use when exchanging the JWT assertion for an access token. 237 * @param clientSecret the client secret to use when exchanging the JWT assertion for an access token. 238 * @param encryptionPref the encryption preferences for signing the JWT. 239 * @return a new instance of BoxAPIConnection. 240 * 241 * @deprecated Use the version of this method that accepts an IAccessTokenCache to prevent unneeded 242 * requests to Box for access tokens. 243 */ 244 @Deprecated 245 public static BoxDeveloperEditionAPIConnection getAppUserConnection(String userId, String clientId, 246 String clientSecret, JWTEncryptionPreferences encryptionPref) { 247 248 BoxDeveloperEditionAPIConnection connection = new BoxDeveloperEditionAPIConnection(userId, 249 DeveloperEditionEntityType.USER, clientId, clientSecret, encryptionPref); 250 251 connection.authenticate(); 252 253 return connection; 254 } 255 256 /** 257 * Creates a new Box Developer Edition connection with App User token. 258 * @param userId the user ID to use for an App User. 259 * @param clientId the client ID to use when exchanging the JWT assertion for an access token. 260 * @param clientSecret the client secret to use when exchanging the JWT assertion for an access token. 261 * @param encryptionPref the encryption preferences for signing the JWT. 262 * @param accessTokenCache the cache for storing access token information (to minimize fetching new tokens) 263 * @return a new instance of BoxAPIConnection. 264 */ 265 public static BoxDeveloperEditionAPIConnection getAppUserConnection(String userId, String clientId, 266 String clientSecret, JWTEncryptionPreferences encryptionPref, IAccessTokenCache accessTokenCache) { 267 268 BoxDeveloperEditionAPIConnection connection = new BoxDeveloperEditionAPIConnection(userId, 269 DeveloperEditionEntityType.USER, clientId, clientSecret, encryptionPref, accessTokenCache); 270 271 connection.tryRestoreUsingAccessTokenCache(); 272 273 return connection; 274 } 275 276 /** 277 * Creates a new Box Developer Edition connection with App User token levaraging BoxConfig. 278 * @param userId the user ID to use for an App User. 279 * @param boxConfig box configuration settings object 280 * @return a new instance of BoxAPIConnection. 281 */ 282 public static BoxDeveloperEditionAPIConnection getAppUserConnection(String userId, BoxConfig boxConfig) { 283 return getAppUserConnection(userId, boxConfig.getClientId(), boxConfig.getClientSecret(), 284 boxConfig.getJWTEncryptionPreferences()); 285 } 286 287 /** 288 * Creates a new Box Developer Edition connection with App User token leveraging BoxConfig and access token cache. 289 * @param userId the user ID to use for an App User. 290 * @param boxConfig box configuration settings object 291 * @param accessTokenCache the cache for storing access token information (to minimize fetching new tokens) 292 * @return a new instance of BoxAPIConnection. 293 */ 294 public static BoxDeveloperEditionAPIConnection getAppUserConnection(String userId, BoxConfig boxConfig, 295 IAccessTokenCache accessTokenCache) { 296 return getAppUserConnection(userId, boxConfig.getClientId(), boxConfig.getClientSecret(), 297 boxConfig.getJWTEncryptionPreferences(), accessTokenCache); 298 } 299 300 /** 301 * Disabling the non-Box Developer Edition authenticate method. 302 * @param authCode an auth code obtained from the first half of the OAuth process. 303 */ 304 public void authenticate(String authCode) { 305 throw new BoxAPIException("BoxDeveloperEditionAPIConnection does not allow authenticating with an auth code."); 306 } 307 308 /** 309 * Authenticates the API connection for Box Developer Edition. 310 */ 311 public void authenticate() { 312 URL url; 313 try { 314 url = new URL(this.getTokenURL()); 315 } catch (MalformedURLException e) { 316 assert false : "An invalid token URL indicates a bug in the SDK."; 317 throw new RuntimeException("An invalid token URL indicates a bug in the SDK.", e); 318 } 319 320 String jwtAssertion = this.constructJWTAssertion(); 321 322 String urlParameters = String.format(JWT_GRANT_TYPE, this.getClientID(), this.getClientSecret(), jwtAssertion); 323 324 BoxAPIRequest request = new BoxAPIRequest(this, url, "POST"); 325 request.shouldAuthenticate(false); 326 request.setBody(urlParameters); 327 328 String json; 329 try { 330 BoxJSONResponse response = (BoxJSONResponse) request.send(); 331 json = response.getJSON(); 332 } catch (BoxAPIException ex) { 333 // Use the Date advertised by the Box server as the current time to synchronize clocks 334 List<String> responseDates = ex.getHeaders().get("Date"); 335 NumericDate currentTime; 336 if (responseDates != null) { 337 String responseDate = responseDates.get(0); 338 SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss zzz"); 339 try { 340 Date date = dateFormat.parse(responseDate); 341 currentTime = NumericDate.fromMilliseconds(date.getTime()); 342 } catch (ParseException e) { 343 currentTime = NumericDate.now(); 344 } 345 } else { 346 currentTime = NumericDate.now(); 347 } 348 349 // Reconstruct the JWT assertion, which regenerates the jti claim, with the new "current" time 350 jwtAssertion = this.constructJWTAssertion(currentTime); 351 urlParameters = String.format(JWT_GRANT_TYPE, this.getClientID(), this.getClientSecret(), jwtAssertion); 352 353 // Re-send the updated request 354 request = new BoxAPIRequest(this, url, "POST"); 355 request.shouldAuthenticate(false); 356 request.setBody(urlParameters); 357 358 BoxJSONResponse response = (BoxJSONResponse) request.send(); 359 json = response.getJSON(); 360 } 361 362 JsonObject jsonObject = JsonObject.readFrom(json); 363 this.setAccessToken(jsonObject.get("access_token").asString()); 364 this.setLastRefresh(System.currentTimeMillis()); 365 this.setExpires(jsonObject.get("expires_in").asLong() * 1000); 366 367 //if token cache is specified, save to cache 368 if (this.accessTokenCache != null) { 369 String key = this.getAccessTokenCacheKey(); 370 JsonObject accessTokenCacheInfo = new JsonObject() 371 .add("accessToken", this.getAccessToken()) 372 .add("lastRefresh", this.getLastRefresh()) 373 .add("expires", this.getExpires()); 374 375 this.accessTokenCache.put(key, accessTokenCacheInfo.toString()); 376 } 377 } 378 379 /** 380 * BoxDeveloperEditionAPIConnection can always refresh, but this method is required elsewhere. 381 * @return true always. 382 */ 383 public boolean canRefresh() { 384 return true; 385 } 386 387 /** 388 * Refresh's this connection's access token using Box Developer Edition. 389 * @throws IllegalStateException if this connection's access token cannot be refreshed. 390 */ 391 public void refresh() { 392 this.getRefreshLock().writeLock().lock(); 393 394 try { 395 this.authenticate(); 396 } catch (BoxAPIException e) { 397 this.notifyError(e); 398 this.getRefreshLock().writeLock().unlock(); 399 throw e; 400 } 401 402 this.notifyRefresh(); 403 this.getRefreshLock().writeLock().unlock(); 404 } 405 406 private String getAccessTokenCacheKey() { 407 return String.format("/%s/%s/%s/%s", this.getUserAgent(), this.getClientID(), 408 this.entityType.toString(), this.entityID); 409 } 410 411 private void tryRestoreUsingAccessTokenCache() { 412 if (this.accessTokenCache == null) { 413 //no cache specified so force authentication 414 this.authenticate(); 415 } else { 416 String cachedTokenInfo = this.accessTokenCache.get(this.getAccessTokenCacheKey()); 417 if (cachedTokenInfo == null) { 418 //not found; probably first time for this client config so authenticate; info will then be cached 419 this.authenticate(); 420 } else { 421 //pull access token cache info; authentication will occur as needed (if token is expired) 422 JsonObject json = JsonObject.readFrom(cachedTokenInfo); 423 this.setAccessToken(json.get("accessToken").asString()); 424 this.setLastRefresh(json.get("lastRefresh").asLong()); 425 this.setExpires(json.get("expires").asLong()); 426 } 427 } 428 } 429 430 private String constructJWTAssertion() { 431 return this.constructJWTAssertion(null); 432 } 433 434 private String constructJWTAssertion(NumericDate now) { 435 JwtClaims claims = new JwtClaims(); 436 claims.setIssuer(this.getClientID()); 437 claims.setAudience(JWT_AUDIENCE); 438 if (now == null) { 439 claims.setExpirationTimeMinutesInTheFuture(0.5f); 440 } else { 441 now.addSeconds(30L); 442 claims.setExpirationTime(now); 443 } 444 claims.setSubject(this.entityID); 445 claims.setClaim("box_sub_type", this.entityType.toString()); 446 claims.setGeneratedJwtId(64); 447 448 JsonWebSignature jws = new JsonWebSignature(); 449 jws.setPayload(claims.toJson()); 450 jws.setKey(this.decryptPrivateKey()); 451 jws.setAlgorithmHeaderValue(this.getAlgorithmIdentifier()); 452 jws.setHeader("typ", "JWT"); 453 if ((this.publicKeyID != null) && !this.publicKeyID.isEmpty()) { 454 jws.setHeader("kid", this.publicKeyID); 455 } 456 457 String assertion; 458 459 try { 460 assertion = jws.getCompactSerialization(); 461 } catch (JoseException e) { 462 throw new BoxAPIException("Error serializing JSON Web Token assertion.", e); 463 } 464 465 return assertion; 466 } 467 468 private String getAlgorithmIdentifier() { 469 String algorithmId = AlgorithmIdentifiers.RSA_USING_SHA256; 470 switch (this.encryptionAlgorithm) { 471 case RSA_SHA_384: 472 algorithmId = AlgorithmIdentifiers.RSA_USING_SHA384; 473 break; 474 case RSA_SHA_512: 475 algorithmId = AlgorithmIdentifiers.RSA_USING_SHA512; 476 break; 477 case RSA_SHA_256: 478 default: 479 break; 480 } 481 482 return algorithmId; 483 } 484 485 private PrivateKey decryptPrivateKey() { 486 PrivateKey decryptedPrivateKey = null; 487 try { 488 PEMParser keyReader = new PEMParser(new StringReader(this.privateKey)); 489 Object keyPair = keyReader.readObject(); 490 keyReader.close(); 491 492 if (keyPair instanceof PrivateKeyInfo) { 493 PrivateKeyInfo keyInfo = (PrivateKeyInfo) keyPair; 494 decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo); 495 } else if (keyPair instanceof PEMEncryptedKeyPair) { 496 JcePEMDecryptorProviderBuilder builder = new JcePEMDecryptorProviderBuilder(); 497 PEMDecryptorProvider decryptionProvider = builder.build(this.privateKeyPassword.toCharArray()); 498 keyPair = ((PEMEncryptedKeyPair) keyPair).decryptKeyPair(decryptionProvider); 499 PrivateKeyInfo keyInfo = ((PEMKeyPair) keyPair).getPrivateKeyInfo(); 500 decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo); 501 } else if (keyPair instanceof PKCS8EncryptedPrivateKeyInfo) { 502 InputDecryptorProvider pkcs8Prov = new JceOpenSSLPKCS8DecryptorProviderBuilder().setProvider("BC") 503 .build(this.privateKeyPassword.toCharArray()); 504 PrivateKeyInfo keyInfo = ((PKCS8EncryptedPrivateKeyInfo) keyPair).decryptPrivateKeyInfo(pkcs8Prov); 505 decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo); 506 } else { 507 PrivateKeyInfo keyInfo = ((PEMKeyPair) keyPair).getPrivateKeyInfo(); 508 decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo); 509 } 510 } catch (IOException e) { 511 throw new BoxAPIException("Error parsing private key for Box Developer Edition.", e); 512 } catch (OperatorCreationException e) { 513 throw new BoxAPIException("Error parsing PKCS#8 private key for Box Developer Edition.", e); 514 } catch (PKCSException e) { 515 throw new BoxAPIException("Error parsing PKCS private key for Box Developer Edition.", e); 516 } 517 return decryptedPrivateKey; 518 } 519 520}