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 private static final String JWT_GRANT_TYPE = 032 "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&client_id=%s&client_secret=%s&assertion=%s"; 033 034 private final String entityID; 035 private final DeveloperEditionEntityType entityType; 036 private final EncryptionAlgorithm encryptionAlgorithm; 037 private final String publicKeyID; 038 private final String privateKey; 039 private final String privateKeyPassword; 040 041 private IAccessTokenCache accessTokenCache; 042 043 /** 044 * Disabling an invalid constructor for Box Developer Edition. 045 * @param accessToken an initial access token to use for authenticating with the API. 046 */ 047 public BoxDeveloperEditionAPIConnection(String accessToken) { 048 super(null); 049 throw new BoxAPIException("This constructor is not available for BoxDeveloperEditionAPIConnection."); 050 } 051 052 /** 053 * Disabling an invalid constructor for Box Developer Edition. 054 * @param clientID the client ID to use when refreshing the access token. 055 * @param clientSecret the client secret to use when refreshing the access token. 056 * @param accessToken an initial access token to use for authenticating with the API. 057 * @param refreshToken an initial refresh token to use when refreshing the access token. 058 */ 059 public BoxDeveloperEditionAPIConnection(String clientID, String clientSecret, String accessToken, 060 String refreshToken) { 061 062 super(null); 063 throw new BoxAPIException("This constructor is not available for BoxDeveloperEditionAPIConnection."); 064 } 065 066 /** 067 * Disabling an invalid constructor for Box Developer Edition. 068 * @param clientID the client ID to use when exchanging the auth code for an access token. 069 * @param clientSecret the client secret to use when exchanging the auth code for an access token. 070 * @param authCode an auth code obtained from the first half of the OAuth process. 071 */ 072 public BoxDeveloperEditionAPIConnection(String clientID, String clientSecret, String authCode) { 073 super(null); 074 throw new BoxAPIException("This constructor is not available for BoxDeveloperEditionAPIConnection."); 075 } 076 077 /** 078 * Disabling an invalid constructor for Box Developer Edition. 079 * @param clientID the client ID to use when requesting an access token. 080 * @param clientSecret the client secret to use when requesting an access token. 081 */ 082 public BoxDeveloperEditionAPIConnection(String clientID, String clientSecret) { 083 super(null); 084 throw new BoxAPIException("This constructor is not available for BoxDeveloperEditionAPIConnection."); 085 } 086 087 /** 088 * Constructs a new BoxDeveloperEditionAPIConnection. 089 * @param entityId enterprise ID or a user ID. 090 * @param entityType the type of entityId. 091 * @param clientID the client ID to use when exchanging the JWT assertion for an access token. 092 * @param clientSecret the client secret to use when exchanging the JWT assertion for an access token. 093 * @param encryptionPref the encryption preferences for signing the JWT. 094 * 095 * @deprecated Use the version of this constructor that accepts an IAccessTokenCache to prevent unneeded 096 * requests to Box for access tokens. 097 */ 098 @Deprecated 099 public BoxDeveloperEditionAPIConnection(String entityId, DeveloperEditionEntityType entityType, 100 String clientID, String clientSecret, JWTEncryptionPreferences encryptionPref) { 101 102 this(entityId, entityType, clientID, clientSecret, encryptionPref, null); 103 } 104 105 106 /** 107 * Constructs a new BoxDeveloperEditionAPIConnection leveraging an access token cache. 108 * @param entityId enterprise ID or a user ID. 109 * @param entityType the type of entityId. 110 * @param clientID the client ID to use when exchanging the JWT assertion for an access token. 111 * @param clientSecret the client secret to use when exchanging the JWT assertion for an access token. 112 * @param encryptionPref the encryption preferences for signing the JWT. 113 * @param accessTokenCache the cache for storing access token information (to minimize fetching new tokens) 114 */ 115 public BoxDeveloperEditionAPIConnection(String entityId, DeveloperEditionEntityType entityType, 116 String clientID, String clientSecret, 117 JWTEncryptionPreferences encryptionPref, 118 IAccessTokenCache accessTokenCache) { 119 120 super(clientID, clientSecret); 121 122 this.entityID = entityId; 123 this.entityType = entityType; 124 this.publicKeyID = encryptionPref.getPublicKeyID(); 125 this.privateKey = encryptionPref.getPrivateKey(); 126 this.privateKeyPassword = encryptionPref.getPrivateKeyPassword(); 127 this.encryptionAlgorithm = encryptionPref.getEncryptionAlgorithm(); 128 this.accessTokenCache = accessTokenCache; 129 } 130 131 /** 132 * Creates a new Box Developer Edition connection with enterprise token. 133 * @param enterpriseId the enterprise ID to use for requesting access token. 134 * @param clientId the client ID to use when exchanging the JWT assertion for an access token. 135 * @param clientSecret the client secret to use when exchanging the JWT assertion for an access token. 136 * @param encryptionPref the encryption preferences for signing the JWT. 137 * @return a new instance of BoxAPIConnection. 138 * 139 * @deprecated Use the version of this method that accepts an IAccessTokenCache to prevent unneeded 140 * requests to Box for access tokens. 141 */ 142 @Deprecated 143 public static BoxDeveloperEditionAPIConnection getAppEnterpriseConnection(String enterpriseId, String clientId, 144 String clientSecret, JWTEncryptionPreferences encryptionPref) { 145 146 BoxDeveloperEditionAPIConnection connection = new BoxDeveloperEditionAPIConnection(enterpriseId, 147 DeveloperEditionEntityType.ENTERPRISE, clientId, clientSecret, encryptionPref); 148 149 connection.authenticate(); 150 151 return connection; 152 } 153 154 /** 155 * Creates a new Box Developer Edition connection with enterprise token leveraging an access token cache. 156 * @param enterpriseId the enterprise ID to use for requesting access token. 157 * @param clientId the client ID to use when exchanging the JWT assertion for an access token. 158 * @param clientSecret the client secret to use when exchanging the JWT assertion for an access token. 159 * @param encryptionPref the encryption preferences for signing the JWT. 160 * @param accessTokenCache the cache for storing access token information (to minimize fetching new tokens) 161 * @return a new instance of BoxAPIConnection. 162 */ 163 public static BoxDeveloperEditionAPIConnection getAppEnterpriseConnection(String enterpriseId, String clientId, 164 String clientSecret, JWTEncryptionPreferences encryptionPref, IAccessTokenCache accessTokenCache) { 165 166 BoxDeveloperEditionAPIConnection connection = new BoxDeveloperEditionAPIConnection(enterpriseId, 167 DeveloperEditionEntityType.ENTERPRISE, clientId, clientSecret, encryptionPref, accessTokenCache); 168 169 connection.tryRestoreUsingAccessTokenCache(); 170 171 return connection; 172 } 173 174 /** 175 * Creates a new Box Developer Edition connection with App User token. 176 * @param userId the user ID to use for an App User. 177 * @param clientId the client ID to use when exchanging the JWT assertion for an access token. 178 * @param clientSecret the client secret to use when exchanging the JWT assertion for an access token. 179 * @param encryptionPref the encryption preferences for signing the JWT. 180 * @return a new instance of BoxAPIConnection. 181 * 182 * @deprecated Use the version of this method that accepts an IAccessTokenCache to prevent unneeded 183 * requests to Box for access tokens. 184 */ 185 @Deprecated 186 public static BoxDeveloperEditionAPIConnection getAppUserConnection(String userId, String clientId, 187 String clientSecret, JWTEncryptionPreferences encryptionPref) { 188 189 BoxDeveloperEditionAPIConnection connection = new BoxDeveloperEditionAPIConnection(userId, 190 DeveloperEditionEntityType.USER, clientId, clientSecret, encryptionPref); 191 192 connection.authenticate(); 193 194 return connection; 195 } 196 197 /** 198 * Creates a new Box Developer Edition connection with App User token. 199 * @param userId the user ID to use for an App User. 200 * @param clientId the client ID to use when exchanging the JWT assertion for an access token. 201 * @param clientSecret the client secret to use when exchanging the JWT assertion for an access token. 202 * @param encryptionPref the encryption preferences for signing the JWT. 203 * @param accessTokenCache the cache for storing access token information (to minimize fetching new tokens) 204 * @return a new instance of BoxAPIConnection. 205 */ 206 public static BoxDeveloperEditionAPIConnection getAppUserConnection(String userId, String clientId, 207 String clientSecret, JWTEncryptionPreferences encryptionPref, IAccessTokenCache accessTokenCache) { 208 209 BoxDeveloperEditionAPIConnection connection = new BoxDeveloperEditionAPIConnection(userId, 210 DeveloperEditionEntityType.USER, clientId, clientSecret, encryptionPref, accessTokenCache); 211 212 connection.tryRestoreUsingAccessTokenCache(); 213 214 return connection; 215 } 216 217 /** 218 * Disabling the non-Box Developer Edition authenticate method. 219 * @param authCode an auth code obtained from the first half of the OAuth process. 220 */ 221 public void authenticate(String authCode) { 222 throw new BoxAPIException("BoxDeveloperEditionAPIConnection does not allow authenticating with an auth code."); 223 } 224 225 /** 226 * Authenticates the API connection for Box Developer Edition. 227 */ 228 public void authenticate() { 229 URL url; 230 try { 231 url = new URL(this.getTokenURL()); 232 } catch (MalformedURLException e) { 233 assert false : "An invalid token URL indicates a bug in the SDK."; 234 throw new RuntimeException("An invalid token URL indicates a bug in the SDK.", e); 235 } 236 237 String jwtAssertion = this.constructJWTAssertion(); 238 239 String urlParameters = String.format(JWT_GRANT_TYPE, this.getClientID(), this.getClientSecret(), jwtAssertion); 240 241 BoxAPIRequest request = new BoxAPIRequest(this, url, "POST"); 242 request.shouldAuthenticate(false); 243 request.setBody(urlParameters); 244 245 BoxJSONResponse response = (BoxJSONResponse) request.send(); 246 String json = response.getJSON(); 247 248 JsonObject jsonObject = JsonObject.readFrom(json); 249 this.setAccessToken(jsonObject.get("access_token").asString()); 250 this.setLastRefresh(System.currentTimeMillis()); 251 this.setExpires(jsonObject.get("expires_in").asLong() * 1000); 252 253 //if token cache is specified, save to cache 254 if (this.accessTokenCache != null) { 255 String key = this.getAccessTokenCacheKey(); 256 JsonObject accessTokenCacheInfo = new JsonObject() 257 .add("accessToken", this.getAccessToken()) 258 .add("lastRefresh", this.getLastRefresh()) 259 .add("expires", this.getExpires()); 260 261 this.accessTokenCache.put(key, accessTokenCacheInfo.toString()); 262 } 263 } 264 265 /** 266 * BoxDeveloperEditionAPIConnection can always refresh, but this method is required elsewhere. 267 * @return true always. 268 */ 269 public boolean canRefresh() { 270 return true; 271 } 272 273 /** 274 * Refresh's this connection's access token using Box Developer Edition. 275 * @throws IllegalStateException if this connection's access token cannot be refreshed. 276 */ 277 public void refresh() { 278 this.getRefreshLock().writeLock().lock(); 279 280 try { 281 this.authenticate(); 282 } catch (BoxAPIException e) { 283 this.notifyError(e); 284 this.getRefreshLock().writeLock().unlock(); 285 throw e; 286 } 287 288 this.notifyRefresh(); 289 this.getRefreshLock().writeLock().unlock(); 290 } 291 292 private String getAccessTokenCacheKey() { 293 return String.format("/%s/%s/%s/%s", this.getUserAgent(), this.getClientID(), 294 this.entityType.toString(), this.entityID); 295 } 296 297 private void tryRestoreUsingAccessTokenCache() { 298 if (this.accessTokenCache == null) { 299 //no cache specified so force authentication 300 this.authenticate(); 301 } else { 302 String cachedTokenInfo = this.accessTokenCache.get(this.getAccessTokenCacheKey()); 303 if (cachedTokenInfo == null) { 304 //not found; probably first time for this client config so authenticate; info will then be cached 305 this.authenticate(); 306 } else { 307 //pull access token cache info; authentication will occur as needed (if token is expired) 308 JsonObject json = JsonObject.readFrom(cachedTokenInfo); 309 this.setAccessToken(json.get("accessToken").asString()); 310 this.setLastRefresh(json.get("lastRefresh").asLong()); 311 this.setExpires(json.get("expires").asLong()); 312 } 313 } 314 } 315 316 private String constructJWTAssertion() { 317 JwtClaims claims = new JwtClaims(); 318 claims.setIssuer(this.getClientID()); 319 claims.setAudience(JWT_AUDIENCE); 320 claims.setExpirationTimeMinutesInTheFuture(1.0f); 321 claims.setSubject(this.entityID); 322 claims.setClaim("box_sub_type", this.entityType.toString()); 323 claims.setGeneratedJwtId(64); 324 325 JsonWebSignature jws = new JsonWebSignature(); 326 jws.setPayload(claims.toJson()); 327 jws.setKey(this.decryptPrivateKey()); 328 jws.setAlgorithmHeaderValue(this.getAlgorithmIdentifier()); 329 jws.setHeader("typ", "JWT"); 330 if ((this.publicKeyID != null) && !this.publicKeyID.isEmpty()) { 331 jws.setHeader("kid", this.publicKeyID); 332 } 333 334 String assertion; 335 336 try { 337 assertion = jws.getCompactSerialization(); 338 } catch (JoseException e) { 339 throw new BoxAPIException("Error serializing JSON Web Token assertion.", e); 340 } 341 342 return assertion; 343 } 344 345 private String getAlgorithmIdentifier() { 346 String algorithmId = AlgorithmIdentifiers.RSA_USING_SHA256; 347 switch (this.encryptionAlgorithm) { 348 case RSA_SHA_384: 349 algorithmId = AlgorithmIdentifiers.RSA_USING_SHA384; 350 break; 351 case RSA_SHA_512: 352 algorithmId = AlgorithmIdentifiers.RSA_USING_SHA512; 353 break; 354 case RSA_SHA_256: 355 default: 356 break; 357 } 358 359 return algorithmId; 360 } 361 362 private PrivateKey decryptPrivateKey() { 363 PrivateKey decryptedPrivateKey; 364 365 try { 366 PEMParser keyReader = new PEMParser(new StringReader(this.privateKey)); 367 Object keyPair = keyReader.readObject(); 368 keyReader.close(); 369 370 if (keyPair instanceof PEMEncryptedKeyPair) { 371 JcePEMDecryptorProviderBuilder builder = new JcePEMDecryptorProviderBuilder(); 372 PEMDecryptorProvider decryptionProvider = builder.build(this.privateKeyPassword.toCharArray()); 373 keyPair = ((PEMEncryptedKeyPair) keyPair).decryptKeyPair(decryptionProvider); 374 } 375 376 PrivateKeyInfo keyInfo = ((PEMKeyPair) keyPair).getPrivateKeyInfo(); 377 decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo); 378 } catch (IOException e) { 379 throw new BoxAPIException("Error parsing private key for Box Developer Edition.", e); 380 } 381 382 return decryptedPrivateKey; 383 } 384 385}