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 * Constructs a new BoxDeveloperEditionAPIConnection. 143 * @param entityId enterprise ID or a user ID. 144 * @param entityType the type of entityId. 145 * @param boxConfig box configuration settings object 146 * @param accessTokenCache the cache for storing access token information (to minimize fetching new tokens) 147 * 148 */ 149 public BoxDeveloperEditionAPIConnection(String entityId, DeveloperEditionEntityType entityType, 150 BoxConfig boxConfig, IAccessTokenCache accessTokenCache) { 151 152 this(entityId, entityType, boxConfig.getClientId(), boxConfig.getClientSecret(), 153 boxConfig.getJWTEncryptionPreferences(), accessTokenCache); 154 } 155 156 /** 157 * Creates a new Box Developer Edition connection with enterprise token. 158 * @param enterpriseId the enterprise ID to use for requesting access token. 159 * @param clientId the client ID to use when exchanging the JWT assertion for an access token. 160 * @param clientSecret the client secret to use when exchanging the JWT assertion for an access token. 161 * @param encryptionPref the encryption preferences for signing the JWT. 162 * @return a new instance of BoxAPIConnection. 163 * 164 * @deprecated Use the version of this method that accepts an IAccessTokenCache to prevent unneeded 165 * requests to Box for access tokens. 166 */ 167 @Deprecated 168 public static BoxDeveloperEditionAPIConnection getAppEnterpriseConnection(String enterpriseId, String clientId, 169 String clientSecret, JWTEncryptionPreferences encryptionPref) { 170 171 BoxDeveloperEditionAPIConnection connection = new BoxDeveloperEditionAPIConnection(enterpriseId, 172 DeveloperEditionEntityType.ENTERPRISE, clientId, clientSecret, encryptionPref); 173 174 connection.authenticate(); 175 176 return connection; 177 } 178 179 /** 180 * Creates a new Box Developer Edition connection with enterprise token leveraging an access token cache. 181 * @param enterpriseId the enterprise ID to use for requesting access token. 182 * @param clientId the client ID to use when exchanging the JWT assertion for an access token. 183 * @param clientSecret the client secret to use when exchanging the JWT assertion for an access token. 184 * @param encryptionPref the encryption preferences for signing the JWT. 185 * @param accessTokenCache the cache for storing access token information (to minimize fetching new tokens) 186 * @return a new instance of BoxAPIConnection. 187 */ 188 public static BoxDeveloperEditionAPIConnection getAppEnterpriseConnection(String enterpriseId, String clientId, 189 String clientSecret, JWTEncryptionPreferences encryptionPref, IAccessTokenCache accessTokenCache) { 190 191 BoxDeveloperEditionAPIConnection connection = new BoxDeveloperEditionAPIConnection(enterpriseId, 192 DeveloperEditionEntityType.ENTERPRISE, clientId, clientSecret, encryptionPref, accessTokenCache); 193 194 connection.tryRestoreUsingAccessTokenCache(); 195 196 return connection; 197 } 198 199 /** 200 * Creates a new Box Developer Edition connection with enterprise token leveraging BoxConfig. 201 * @param boxConfig box configuration settings object 202 * @return a new instance of BoxAPIConnection. 203 */ 204 public static BoxDeveloperEditionAPIConnection getAppEnterpriseConnection(BoxConfig boxConfig) { 205 206 BoxDeveloperEditionAPIConnection connection = getAppEnterpriseConnection(boxConfig.getEnterpriseId(), 207 boxConfig.getClientId(), boxConfig.getClientSecret(), boxConfig.getJWTEncryptionPreferences()); 208 209 return connection; 210 } 211 212 /** 213 * Creates a new Box Developer Edition connection with enterprise token leveraging BoxConfig and access token cache. 214 * @param boxConfig box configuration settings object 215 * @param accessTokenCache the cache for storing access token information (to minimize fetching new tokens) 216 * @return a new instance of BoxAPIConnection. 217 */ 218 public static BoxDeveloperEditionAPIConnection getAppEnterpriseConnection(BoxConfig boxConfig, 219 IAccessTokenCache accessTokenCache) { 220 221 BoxDeveloperEditionAPIConnection connection = getAppEnterpriseConnection(boxConfig.getEnterpriseId(), 222 boxConfig.getClientId(), boxConfig.getClientSecret(), boxConfig.getJWTEncryptionPreferences(), 223 accessTokenCache); 224 225 return connection; 226 } 227 228 /** 229 * Creates a new Box Developer Edition connection with App User token. 230 * @param userId the user ID to use for an App User. 231 * @param clientId the client ID to use when exchanging the JWT assertion for an access token. 232 * @param clientSecret the client secret to use when exchanging the JWT assertion for an access token. 233 * @param encryptionPref the encryption preferences for signing the JWT. 234 * @return a new instance of BoxAPIConnection. 235 * 236 * @deprecated Use the version of this method that accepts an IAccessTokenCache to prevent unneeded 237 * requests to Box for access tokens. 238 */ 239 @Deprecated 240 public static BoxDeveloperEditionAPIConnection getAppUserConnection(String userId, String clientId, 241 String clientSecret, JWTEncryptionPreferences encryptionPref) { 242 243 BoxDeveloperEditionAPIConnection connection = new BoxDeveloperEditionAPIConnection(userId, 244 DeveloperEditionEntityType.USER, clientId, clientSecret, encryptionPref); 245 246 connection.authenticate(); 247 248 return connection; 249 } 250 251 /** 252 * Creates a new Box Developer Edition connection with App User token. 253 * @param userId the user ID to use for an App User. 254 * @param clientId the client ID to use when exchanging the JWT assertion for an access token. 255 * @param clientSecret the client secret to use when exchanging the JWT assertion for an access token. 256 * @param encryptionPref the encryption preferences for signing the JWT. 257 * @param accessTokenCache the cache for storing access token information (to minimize fetching new tokens) 258 * @return a new instance of BoxAPIConnection. 259 */ 260 public static BoxDeveloperEditionAPIConnection getAppUserConnection(String userId, String clientId, 261 String clientSecret, JWTEncryptionPreferences encryptionPref, IAccessTokenCache accessTokenCache) { 262 263 BoxDeveloperEditionAPIConnection connection = new BoxDeveloperEditionAPIConnection(userId, 264 DeveloperEditionEntityType.USER, clientId, clientSecret, encryptionPref, accessTokenCache); 265 266 connection.tryRestoreUsingAccessTokenCache(); 267 268 return connection; 269 } 270 271 /** 272 * Creates a new Box Developer Edition connection with App User token levaraging BoxConfig. 273 * @param userId the user ID to use for an App User. 274 * @param boxConfig box configuration settings object 275 * @return a new instance of BoxAPIConnection. 276 */ 277 public static BoxDeveloperEditionAPIConnection getAppUserConnection(String userId, BoxConfig boxConfig) { 278 return getAppUserConnection(userId, boxConfig.getClientId(), boxConfig.getClientSecret(), 279 boxConfig.getJWTEncryptionPreferences()); 280 } 281 282 /** 283 * Creates a new Box Developer Edition connection with App User token leveraging BoxConfig and access token cache. 284 * @param userId the user ID to use for an App User. 285 * @param boxConfig box configuration settings object 286 * @param accessTokenCache the cache for storing access token information (to minimize fetching new tokens) 287 * @return a new instance of BoxAPIConnection. 288 */ 289 public static BoxDeveloperEditionAPIConnection getAppUserConnection(String userId, BoxConfig boxConfig, 290 IAccessTokenCache accessTokenCache) { 291 return getAppUserConnection(userId, boxConfig.getClientId(), boxConfig.getClientSecret(), 292 boxConfig.getJWTEncryptionPreferences(), accessTokenCache); 293 } 294 295 /** 296 * Disabling the non-Box Developer Edition authenticate method. 297 * @param authCode an auth code obtained from the first half of the OAuth process. 298 */ 299 public void authenticate(String authCode) { 300 throw new BoxAPIException("BoxDeveloperEditionAPIConnection does not allow authenticating with an auth code."); 301 } 302 303 /** 304 * Authenticates the API connection for Box Developer Edition. 305 */ 306 public void authenticate() { 307 URL url; 308 try { 309 url = new URL(this.getTokenURL()); 310 } catch (MalformedURLException e) { 311 assert false : "An invalid token URL indicates a bug in the SDK."; 312 throw new RuntimeException("An invalid token URL indicates a bug in the SDK.", e); 313 } 314 315 String jwtAssertion = this.constructJWTAssertion(); 316 317 String urlParameters = String.format(JWT_GRANT_TYPE, this.getClientID(), this.getClientSecret(), jwtAssertion); 318 319 BoxAPIRequest request = new BoxAPIRequest(this, url, "POST"); 320 request.shouldAuthenticate(false); 321 request.setBody(urlParameters); 322 323 BoxJSONResponse response = (BoxJSONResponse) request.send(); 324 String json = response.getJSON(); 325 326 JsonObject jsonObject = JsonObject.readFrom(json); 327 this.setAccessToken(jsonObject.get("access_token").asString()); 328 this.setLastRefresh(System.currentTimeMillis()); 329 this.setExpires(jsonObject.get("expires_in").asLong() * 1000); 330 331 //if token cache is specified, save to cache 332 if (this.accessTokenCache != null) { 333 String key = this.getAccessTokenCacheKey(); 334 JsonObject accessTokenCacheInfo = new JsonObject() 335 .add("accessToken", this.getAccessToken()) 336 .add("lastRefresh", this.getLastRefresh()) 337 .add("expires", this.getExpires()); 338 339 this.accessTokenCache.put(key, accessTokenCacheInfo.toString()); 340 } 341 } 342 343 /** 344 * BoxDeveloperEditionAPIConnection can always refresh, but this method is required elsewhere. 345 * @return true always. 346 */ 347 public boolean canRefresh() { 348 return true; 349 } 350 351 /** 352 * Refresh's this connection's access token using Box Developer Edition. 353 * @throws IllegalStateException if this connection's access token cannot be refreshed. 354 */ 355 public void refresh() { 356 this.getRefreshLock().writeLock().lock(); 357 358 try { 359 this.authenticate(); 360 } catch (BoxAPIException e) { 361 this.notifyError(e); 362 this.getRefreshLock().writeLock().unlock(); 363 throw e; 364 } 365 366 this.notifyRefresh(); 367 this.getRefreshLock().writeLock().unlock(); 368 } 369 370 private String getAccessTokenCacheKey() { 371 return String.format("/%s/%s/%s/%s", this.getUserAgent(), this.getClientID(), 372 this.entityType.toString(), this.entityID); 373 } 374 375 private void tryRestoreUsingAccessTokenCache() { 376 if (this.accessTokenCache == null) { 377 //no cache specified so force authentication 378 this.authenticate(); 379 } else { 380 String cachedTokenInfo = this.accessTokenCache.get(this.getAccessTokenCacheKey()); 381 if (cachedTokenInfo == null) { 382 //not found; probably first time for this client config so authenticate; info will then be cached 383 this.authenticate(); 384 } else { 385 //pull access token cache info; authentication will occur as needed (if token is expired) 386 JsonObject json = JsonObject.readFrom(cachedTokenInfo); 387 this.setAccessToken(json.get("accessToken").asString()); 388 this.setLastRefresh(json.get("lastRefresh").asLong()); 389 this.setExpires(json.get("expires").asLong()); 390 } 391 } 392 } 393 394 private String constructJWTAssertion() { 395 JwtClaims claims = new JwtClaims(); 396 claims.setIssuer(this.getClientID()); 397 claims.setAudience(JWT_AUDIENCE); 398 claims.setExpirationTimeMinutesInTheFuture(1.0f); 399 claims.setSubject(this.entityID); 400 claims.setClaim("box_sub_type", this.entityType.toString()); 401 claims.setGeneratedJwtId(64); 402 403 JsonWebSignature jws = new JsonWebSignature(); 404 jws.setPayload(claims.toJson()); 405 jws.setKey(this.decryptPrivateKey()); 406 jws.setAlgorithmHeaderValue(this.getAlgorithmIdentifier()); 407 jws.setHeader("typ", "JWT"); 408 if ((this.publicKeyID != null) && !this.publicKeyID.isEmpty()) { 409 jws.setHeader("kid", this.publicKeyID); 410 } 411 412 String assertion; 413 414 try { 415 assertion = jws.getCompactSerialization(); 416 } catch (JoseException e) { 417 throw new BoxAPIException("Error serializing JSON Web Token assertion.", e); 418 } 419 420 return assertion; 421 } 422 423 private String getAlgorithmIdentifier() { 424 String algorithmId = AlgorithmIdentifiers.RSA_USING_SHA256; 425 switch (this.encryptionAlgorithm) { 426 case RSA_SHA_384: 427 algorithmId = AlgorithmIdentifiers.RSA_USING_SHA384; 428 break; 429 case RSA_SHA_512: 430 algorithmId = AlgorithmIdentifiers.RSA_USING_SHA512; 431 break; 432 case RSA_SHA_256: 433 default: 434 break; 435 } 436 437 return algorithmId; 438 } 439 440 private PrivateKey decryptPrivateKey() { 441 PrivateKey decryptedPrivateKey = null; 442 try { 443 PEMParser keyReader = new PEMParser(new StringReader(this.privateKey)); 444 Object keyPair = keyReader.readObject(); 445 keyReader.close(); 446 447 if (keyPair instanceof PrivateKeyInfo) { 448 PrivateKeyInfo keyInfo = (PrivateKeyInfo) keyPair; 449 decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo); 450 } else if (keyPair instanceof PEMEncryptedKeyPair) { 451 JcePEMDecryptorProviderBuilder builder = new JcePEMDecryptorProviderBuilder(); 452 PEMDecryptorProvider decryptionProvider = builder.build(this.privateKeyPassword.toCharArray()); 453 keyPair = ((PEMEncryptedKeyPair) keyPair).decryptKeyPair(decryptionProvider); 454 PrivateKeyInfo keyInfo = ((PEMKeyPair) keyPair).getPrivateKeyInfo(); 455 decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo); 456 } else if (keyPair instanceof PKCS8EncryptedPrivateKeyInfo) { 457 InputDecryptorProvider pkcs8Prov = new JceOpenSSLPKCS8DecryptorProviderBuilder().setProvider("BC") 458 .build(this.privateKeyPassword.toCharArray()); 459 PrivateKeyInfo keyInfo = ((PKCS8EncryptedPrivateKeyInfo) keyPair).decryptPrivateKeyInfo(pkcs8Prov); 460 decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo); 461 } else { 462 PrivateKeyInfo keyInfo = ((PEMKeyPair) keyPair).getPrivateKeyInfo(); 463 decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo); 464 } 465 } catch (IOException e) { 466 throw new BoxAPIException("Error parsing private key for Box Developer Edition.", e); 467 } catch (OperatorCreationException e) { 468 throw new BoxAPIException("Error parsing PKCS#8 private key for Box Developer Edition.", e); 469 } catch (PKCSException e) { 470 throw new BoxAPIException("Error parsing PKCS private key for Box Developer Edition.", e); 471 } 472 return decryptedPrivateKey; 473 } 474 475}