001package com.box.sdk; 002 003import java.net.MalformedURLException; 004import java.net.Proxy; 005import java.net.URI; 006import java.net.URL; 007import java.util.ArrayList; 008import java.util.HashMap; 009import java.util.List; 010import java.util.Map; 011import java.util.concurrent.locks.ReadWriteLock; 012import java.util.concurrent.locks.ReentrantReadWriteLock; 013 014import com.eclipsesource.json.JsonObject; 015 016/** 017 * Represents an authenticated connection to the Box API. 018 * 019 * <p>This class handles storing authentication information, automatic token refresh, and rate-limiting. It can also be 020 * used to configure the Box API endpoint URL in order to hit a different version of the API. Multiple instances of 021 * BoxAPIConnection may be created to support multi-user login.</p> 022 */ 023public class BoxAPIConnection { 024 /** 025 * The default total maximum number of times an API request will be tried when error responses 026 * are received. 027 * @deprecated DEFAULT_MAX_RETRIES is preferred because it more clearly sets the number 028 * of times a request should be retried after an error response is received. 029 */ 030 @Deprecated 031 public static final int DEFAULT_MAX_ATTEMPTS = 5; 032 033 /** 034 * The default maximum number of times an API request will be retried after an error response 035 * is received. 036 */ 037 public static final int DEFAULT_MAX_RETRIES = 5; 038 039 private static final String AUTHORIZATION_URL = "https://account.box.com/api/oauth2/authorize"; 040 private static final String TOKEN_URL_STRING = "https://api.box.com/oauth2/token"; 041 private static final String REVOKE_URL_STRING = "https://api.box.com/oauth2/revoke"; 042 private static final String DEFAULT_BASE_URL = "https://api.box.com/2.0/"; 043 private static final String DEFAULT_BASE_UPLOAD_URL = "https://upload.box.com/api/2.0/"; 044 045 private static final String AS_USER_HEADER = "As-User"; 046 private static final String BOX_NOTIFICATIONS_HEADER = "Box-Notifications"; 047 048 private static final String JAVA_VERSION = System.getProperty("java.version"); 049 private static final String SDK_VERSION = "2.47.0"; 050 051 /** 052 * The amount of buffer time, in milliseconds, to use when determining if an access token should be refreshed. For 053 * example, if REFRESH_EPSILON = 60000 and the access token expires in less than one minute, it will be refreshed. 054 */ 055 private static final long REFRESH_EPSILON = 60000; 056 057 private final String clientID; 058 private final String clientSecret; 059 private final ReadWriteLock refreshLock; 060 061 // These volatile fields are used when determining if the access token needs to be refreshed. Since they are used in 062 // the double-checked lock in getAccessToken(), they must be atomic. 063 private volatile long lastRefresh; 064 private volatile long expires; 065 066 private Proxy proxy; 067 private String proxyUsername; 068 private String proxyPassword; 069 070 private String userAgent; 071 private String accessToken; 072 private String refreshToken; 073 private String tokenURL; 074 private String revokeURL; 075 private String baseURL; 076 private String baseUploadURL; 077 private boolean autoRefresh; 078 private int maxRetryAttempts; 079 private int connectTimeout; 080 private int readTimeout; 081 private List<BoxAPIConnectionListener> listeners; 082 private RequestInterceptor interceptor; 083 private Map<String, String> customHeaders; 084 085 /** 086 * Constructs a new BoxAPIConnection that authenticates with a developer or access token. 087 * @param accessToken a developer or access token to use for authenticating with the API. 088 */ 089 public BoxAPIConnection(String accessToken) { 090 this(null, null, accessToken, null); 091 } 092 093 /** 094 * Constructs a new BoxAPIConnection with an access token that can be refreshed. 095 * @param clientID the client ID to use when refreshing the access token. 096 * @param clientSecret the client secret to use when refreshing the access token. 097 * @param accessToken an initial access token to use for authenticating with the API. 098 * @param refreshToken an initial refresh token to use when refreshing the access token. 099 */ 100 public BoxAPIConnection(String clientID, String clientSecret, String accessToken, String refreshToken) { 101 this.clientID = clientID; 102 this.clientSecret = clientSecret; 103 this.accessToken = accessToken; 104 this.refreshToken = refreshToken; 105 this.tokenURL = TOKEN_URL_STRING; 106 this.revokeURL = REVOKE_URL_STRING; 107 this.baseURL = DEFAULT_BASE_URL; 108 this.baseUploadURL = DEFAULT_BASE_UPLOAD_URL; 109 this.autoRefresh = true; 110 this.maxRetryAttempts = BoxGlobalSettings.getMaxRetryAttempts(); 111 this.connectTimeout = BoxGlobalSettings.getConnectTimeout(); 112 this.readTimeout = BoxGlobalSettings.getReadTimeout(); 113 this.refreshLock = new ReentrantReadWriteLock(); 114 this.userAgent = "Box Java SDK v" + SDK_VERSION + " (Java " + JAVA_VERSION + ")"; 115 this.listeners = new ArrayList<BoxAPIConnectionListener>(); 116 this.customHeaders = new HashMap<String, String>(); 117 } 118 119 /** 120 * Constructs a new BoxAPIConnection with an auth code that was obtained from the first half of OAuth. 121 * @param clientID the client ID to use when exchanging the auth code for an access token. 122 * @param clientSecret the client secret to use when exchanging the auth code for an access token. 123 * @param authCode an auth code obtained from the first half of the OAuth process. 124 */ 125 public BoxAPIConnection(String clientID, String clientSecret, String authCode) { 126 this(clientID, clientSecret, null, null); 127 this.authenticate(authCode); 128 } 129 130 /** 131 * Constructs a new BoxAPIConnection. 132 * @param clientID the client ID to use when exchanging the auth code for an access token. 133 * @param clientSecret the client secret to use when exchanging the auth code for an access token. 134 */ 135 public BoxAPIConnection(String clientID, String clientSecret) { 136 this(clientID, clientSecret, null, null); 137 } 138 139 /** 140 * Constructs a new BoxAPIConnection levaraging BoxConfig. 141 * @param boxConfig BoxConfig file, which should have clientId and clientSecret 142 */ 143 public BoxAPIConnection(BoxConfig boxConfig) { 144 this(boxConfig.getClientId(), boxConfig.getClientSecret(), null, null); 145 } 146 147 /** 148 * Restores a BoxAPIConnection from a saved state. 149 * 150 * @see #save 151 * @param clientID the client ID to use with the connection. 152 * @param clientSecret the client secret to use with the connection. 153 * @param state the saved state that was created with {@link #save}. 154 * @return a restored API connection. 155 */ 156 public static BoxAPIConnection restore(String clientID, String clientSecret, String state) { 157 BoxAPIConnection api = new BoxAPIConnection(clientID, clientSecret); 158 api.restore(state); 159 return api; 160 } 161 162 /** 163 * Return the authorization URL which is used to perform the authorization_code based OAuth2 flow. 164 * @param clientID the client ID to use with the connection. 165 * @param redirectUri the URL to which Box redirects the browser when authentication completes. 166 * @param state the text string that you choose. 167 * Box sends the same string to your redirect URL when authentication is complete. 168 * @param scopes this optional parameter identifies the Box scopes available 169 * to the application once it's authenticated. 170 * @return the authorization URL 171 */ 172 public static URL getAuthorizationURL(String clientID, URI redirectUri, String state, List<String> scopes) { 173 URLTemplate template = new URLTemplate(AUTHORIZATION_URL); 174 QueryStringBuilder queryBuilder = new QueryStringBuilder().appendParam("client_id", clientID) 175 .appendParam("response_type", "code") 176 .appendParam("redirect_uri", redirectUri.toString()) 177 .appendParam("state", state); 178 179 if (scopes != null && !scopes.isEmpty()) { 180 StringBuilder builder = new StringBuilder(); 181 int size = scopes.size() - 1; 182 int i = 0; 183 while (i < size) { 184 builder.append(scopes.get(i)); 185 builder.append(" "); 186 i++; 187 } 188 builder.append(scopes.get(i)); 189 190 queryBuilder.appendParam("scope", builder.toString()); 191 } 192 193 return template.buildWithQuery("", queryBuilder.toString()); 194 } 195 196 /** 197 * Authenticates the API connection by obtaining access and refresh tokens using the auth code that was obtained 198 * from the first half of OAuth. 199 * @param authCode the auth code obtained from the first half of the OAuth process. 200 */ 201 public void authenticate(String authCode) { 202 URL url = null; 203 try { 204 url = new URL(this.tokenURL); 205 } catch (MalformedURLException e) { 206 assert false : "An invalid token URL indicates a bug in the SDK."; 207 throw new RuntimeException("An invalid token URL indicates a bug in the SDK.", e); 208 } 209 210 String urlParameters = String.format("grant_type=authorization_code&code=%s&client_id=%s&client_secret=%s", 211 authCode, this.clientID, this.clientSecret); 212 213 BoxAPIRequest request = new BoxAPIRequest(this, url, "POST"); 214 request.shouldAuthenticate(false); 215 request.setBody(urlParameters); 216 217 BoxJSONResponse response = (BoxJSONResponse) request.send(); 218 String json = response.getJSON(); 219 220 JsonObject jsonObject = JsonObject.readFrom(json); 221 this.accessToken = jsonObject.get("access_token").asString(); 222 this.refreshToken = jsonObject.get("refresh_token").asString(); 223 this.lastRefresh = System.currentTimeMillis(); 224 this.expires = jsonObject.get("expires_in").asLong() * 1000; 225 } 226 227 /** 228 * Gets the client ID. 229 * @return the client ID. 230 */ 231 public String getClientID() { 232 return this.clientID; 233 } 234 235 /** 236 * Gets the client secret. 237 * @return the client secret. 238 */ 239 public String getClientSecret() { 240 return this.clientSecret; 241 } 242 243 /** 244 * Sets the amount of time for which this connection's access token is valid before it must be refreshed. 245 * @param milliseconds the number of milliseconds for which the access token is valid. 246 */ 247 public void setExpires(long milliseconds) { 248 this.expires = milliseconds; 249 } 250 251 /** 252 * Gets the amount of time for which this connection's access token is valid. 253 * @return the amount of time in milliseconds. 254 */ 255 public long getExpires() { 256 return this.expires; 257 } 258 259 /** 260 * Gets the token URL that's used to request access tokens. The default value is 261 * "https://www.box.com/api/oauth2/token". 262 * @return the token URL. 263 */ 264 public String getTokenURL() { 265 return this.tokenURL; 266 } 267 268 /** 269 * Sets the token URL that's used to request access tokens. For example, the default token URL is 270 * "https://www.box.com/api/oauth2/token". 271 * @param tokenURL the token URL. 272 */ 273 public void setTokenURL(String tokenURL) { 274 this.tokenURL = tokenURL; 275 } 276 277 /** 278 * Set the URL used for token revocation. 279 * @param url The url to use. 280 */ 281 public void setRevokeURL(String url) { 282 this.revokeURL = url; 283 } 284 285 /** 286 * Returns the URL used for token revocation. 287 * @return The url used for token revocation. 288 */ 289 public String getRevokeURL() { 290 return this.revokeURL; 291 } 292 293 /** 294 * Gets the base URL that's used when sending requests to the Box API. The default value is 295 * "https://api.box.com/2.0/". 296 * @return the base URL. 297 */ 298 public String getBaseURL() { 299 return this.baseURL; 300 } 301 302 /** 303 * Sets the base URL to be used when sending requests to the Box API. For example, the default base URL is 304 * "https://api.box.com/2.0/". 305 * @param baseURL a base URL 306 */ 307 public void setBaseURL(String baseURL) { 308 this.baseURL = baseURL; 309 } 310 311 /** 312 * Gets the base upload URL that's used when performing file uploads to Box. 313 * @return the base upload URL. 314 */ 315 public String getBaseUploadURL() { 316 return this.baseUploadURL; 317 } 318 319 /** 320 * Sets the base upload URL to be used when performing file uploads to Box. 321 * @param baseUploadURL a base upload URL. 322 */ 323 public void setBaseUploadURL(String baseUploadURL) { 324 this.baseUploadURL = baseUploadURL; 325 } 326 327 /** 328 * Gets the user agent that's used when sending requests to the Box API. 329 * @return the user agent. 330 */ 331 public String getUserAgent() { 332 return this.userAgent; 333 } 334 335 /** 336 * Sets the user agent to be used when sending requests to the Box API. 337 * @param userAgent the user agent. 338 */ 339 public void setUserAgent(String userAgent) { 340 this.userAgent = userAgent; 341 } 342 343 /** 344 * Gets an access token that can be used to authenticate an API request. This method will automatically refresh the 345 * access token if it has expired since the last call to <code>getAccessToken()</code>. 346 * @return a valid access token that can be used to authenticate an API request. 347 */ 348 public String getAccessToken() { 349 if (this.autoRefresh && this.canRefresh() && this.needsRefresh()) { 350 this.refreshLock.writeLock().lock(); 351 try { 352 if (this.needsRefresh()) { 353 this.refresh(); 354 } 355 } finally { 356 this.refreshLock.writeLock().unlock(); 357 } 358 } 359 360 return this.accessToken; 361 } 362 363 /** 364 * Sets the access token to use when authenticating API requests. 365 * @param accessToken a valid access token to use when authenticating API requests. 366 */ 367 public void setAccessToken(String accessToken) { 368 this.accessToken = accessToken; 369 } 370 371 /** 372 * Gets the refresh lock to be used when refreshing an access token. 373 * @return the refresh lock. 374 */ 375 protected ReadWriteLock getRefreshLock() { 376 return this.refreshLock; 377 } 378 /** 379 * Gets a refresh token that can be used to refresh an access token. 380 * @return a valid refresh token. 381 */ 382 public String getRefreshToken() { 383 return this.refreshToken; 384 } 385 386 /** 387 * Sets the refresh token to use when refreshing an access token. 388 * @param refreshToken a valid refresh token. 389 */ 390 public void setRefreshToken(String refreshToken) { 391 this.refreshToken = refreshToken; 392 } 393 394 /** 395 * Gets the last time that the access token was refreshed. 396 * 397 * @return the last refresh time in milliseconds. 398 */ 399 public long getLastRefresh() { 400 return this.lastRefresh; 401 } 402 403 /** 404 * Sets the last time that the access token was refreshed. 405 * 406 * <p>This value is used when determining if an access token needs to be auto-refreshed. If the amount of time since 407 * the last refresh exceeds the access token's expiration time, then the access token will be refreshed.</p> 408 * 409 * @param lastRefresh the new last refresh time in milliseconds. 410 */ 411 public void setLastRefresh(long lastRefresh) { 412 this.lastRefresh = lastRefresh; 413 } 414 415 /** 416 * Enables or disables automatic refreshing of this connection's access token. Defaults to true. 417 * @param autoRefresh true to enable auto token refresh; otherwise false. 418 */ 419 public void setAutoRefresh(boolean autoRefresh) { 420 this.autoRefresh = autoRefresh; 421 } 422 423 /** 424 * Gets whether or not automatic refreshing of this connection's access token is enabled. Defaults to true. 425 * @return true if auto token refresh is enabled; otherwise false. 426 */ 427 public boolean getAutoRefresh() { 428 return this.autoRefresh; 429 } 430 431 /** 432 * Sets the total maximum number of times an API request will be tried when error responses 433 * are received. 434 * @return the maximum number of request attempts. 435 * @deprecated getMaxRetryAttempts is preferred because it more clearly gets the number 436 * of times a request should be retried after an error response is received. 437 */ 438 @Deprecated 439 public int getMaxRequestAttempts() { 440 return this.maxRetryAttempts + 1; 441 } 442 443 /** 444 * Sets the total maximum number of times an API request will be tried when error responses 445 * are received. 446 * @param attempts the maximum number of request attempts. 447 * @deprecated setMaxRetryAttempts is preferred because it more clearly sets the number 448 * of times a request should be retried after an error response is received. 449 */ 450 @Deprecated 451 public void setMaxRequestAttempts(int attempts) { 452 this.maxRetryAttempts = attempts - 1; 453 } 454 455 /** 456 * Gets the maximum number of times an API request will be retried after an error response 457 * is received. 458 * @return the maximum number of request attempts. 459 */ 460 public int getMaxRetryAttempts() { 461 return this.maxRetryAttempts; 462 } 463 464 /** 465 * Sets the maximum number of times an API request will be retried after an error response 466 * is received. 467 * @param attempts the maximum number of request attempts. 468 */ 469 public void setMaxRetryAttempts(int attempts) { 470 this.maxRetryAttempts = attempts; 471 } 472 473 /** 474 * Gets the connect timeout for this connection in milliseconds. 475 * @return the number of milliseconds to connect before timing out. 476 */ 477 public int getConnectTimeout() { 478 return this.connectTimeout; 479 } 480 481 /** 482 * Sets the connect timeout for this connection. 483 * @param connectTimeout The number of milliseconds to wait for the connection to be established. 484 */ 485 public void setConnectTimeout(int connectTimeout) { 486 this.connectTimeout = connectTimeout; 487 } 488 489 /** 490 * Gets the read timeout for this connection in milliseconds. 491 * @return the number of milliseconds to wait for bytes to be read before timing out. 492 */ 493 public int getReadTimeout() { 494 return this.readTimeout; 495 } 496 497 /** 498 * Sets the read timeout for this connection. 499 * @param readTimeout The number of milliseconds to wait for bytes to be read. 500 */ 501 public void setReadTimeout(int readTimeout) { 502 this.readTimeout = readTimeout; 503 } 504 505 /** 506 * Gets the proxy value to use for API calls to Box. 507 * @return the current proxy. 508 */ 509 public Proxy getProxy() { 510 return this.proxy; 511 } 512 513 /** 514 * Sets the proxy to use for API calls to Box. 515 * @param proxy the proxy to use for API calls to Box. 516 */ 517 public void setProxy(Proxy proxy) { 518 this.proxy = proxy; 519 } 520 521 /** 522 * Gets the username to use for a proxy that requires basic auth. 523 * @return the username to use for a proxy that requires basic auth. 524 */ 525 public String getProxyUsername() { 526 return this.proxyUsername; 527 } 528 529 /** 530 * Sets the username to use for a proxy that requires basic auth. 531 * @param proxyUsername the username to use for a proxy that requires basic auth. 532 */ 533 public void setProxyUsername(String proxyUsername) { 534 this.proxyUsername = proxyUsername; 535 } 536 537 /** 538 * Gets the password to use for a proxy that requires basic auth. 539 * @return the password to use for a proxy that requires basic auth. 540 */ 541 public String getProxyPassword() { 542 return this.proxyPassword; 543 } 544 545 /** 546 * Sets the password to use for a proxy that requires basic auth. 547 * @param proxyPassword the password to use for a proxy that requires basic auth. 548 */ 549 public void setProxyPassword(String proxyPassword) { 550 this.proxyPassword = proxyPassword; 551 } 552 553 /** 554 * Determines if this connection's access token can be refreshed. An access token cannot be refreshed if a refresh 555 * token was never set. 556 * @return true if the access token can be refreshed; otherwise false. 557 */ 558 public boolean canRefresh() { 559 return this.refreshToken != null; 560 } 561 562 /** 563 * Determines if this connection's access token has expired and needs to be refreshed. 564 * @return true if the access token needs to be refreshed; otherwise false. 565 */ 566 public boolean needsRefresh() { 567 boolean needsRefresh; 568 569 this.refreshLock.readLock().lock(); 570 long now = System.currentTimeMillis(); 571 long tokenDuration = (now - this.lastRefresh); 572 needsRefresh = (tokenDuration >= this.expires - REFRESH_EPSILON); 573 this.refreshLock.readLock().unlock(); 574 575 return needsRefresh; 576 } 577 578 /** 579 * Refresh's this connection's access token using its refresh token. 580 * @throws IllegalStateException if this connection's access token cannot be refreshed. 581 */ 582 public void refresh() { 583 this.refreshLock.writeLock().lock(); 584 585 if (!this.canRefresh()) { 586 this.refreshLock.writeLock().unlock(); 587 throw new IllegalStateException("The BoxAPIConnection cannot be refreshed because it doesn't have a " 588 + "refresh token."); 589 } 590 591 URL url = null; 592 try { 593 url = new URL(this.tokenURL); 594 } catch (MalformedURLException e) { 595 this.refreshLock.writeLock().unlock(); 596 assert false : "An invalid refresh URL indicates a bug in the SDK."; 597 throw new RuntimeException("An invalid refresh URL indicates a bug in the SDK.", e); 598 } 599 600 String urlParameters = String.format("grant_type=refresh_token&refresh_token=%s&client_id=%s&client_secret=%s", 601 this.refreshToken, this.clientID, this.clientSecret); 602 603 BoxAPIRequest request = new BoxAPIRequest(this, url, "POST"); 604 request.shouldAuthenticate(false); 605 request.setBody(urlParameters); 606 607 String json; 608 try { 609 BoxJSONResponse response = (BoxJSONResponse) request.send(); 610 json = response.getJSON(); 611 } catch (BoxAPIException e) { 612 this.notifyError(e); 613 this.refreshLock.writeLock().unlock(); 614 throw e; 615 } 616 617 JsonObject jsonObject = JsonObject.readFrom(json); 618 this.accessToken = jsonObject.get("access_token").asString(); 619 this.refreshToken = jsonObject.get("refresh_token").asString(); 620 this.lastRefresh = System.currentTimeMillis(); 621 this.expires = jsonObject.get("expires_in").asLong() * 1000; 622 623 this.notifyRefresh(); 624 625 this.refreshLock.writeLock().unlock(); 626 } 627 628 /** 629 * Restores a saved connection state into this BoxAPIConnection. 630 * 631 * @see #save 632 * @param state the saved state that was created with {@link #save}. 633 */ 634 public void restore(String state) { 635 JsonObject json = JsonObject.readFrom(state); 636 String accessToken = json.get("accessToken").asString(); 637 String refreshToken = json.get("refreshToken").asString(); 638 long lastRefresh = json.get("lastRefresh").asLong(); 639 long expires = json.get("expires").asLong(); 640 String userAgent = json.get("userAgent").asString(); 641 String tokenURL = json.get("tokenURL").asString(); 642 String baseURL = json.get("baseURL").asString(); 643 String baseUploadURL = json.get("baseUploadURL").asString(); 644 boolean autoRefresh = json.get("autoRefresh").asBoolean(); 645 646 // Try to read deprecated value 647 int maxRequestAttempts = -1; 648 if (json.names().contains("maxRequestAttempts")) { 649 maxRequestAttempts = json.get("maxRequestAttempts").asInt(); 650 } 651 652 int maxRetryAttempts = -1; 653 if (json.names().contains("maxRetryAttempts")) { 654 maxRetryAttempts = json.get("maxRetryAttempts").asInt(); 655 } 656 657 this.accessToken = accessToken; 658 this.refreshToken = refreshToken; 659 this.lastRefresh = lastRefresh; 660 this.expires = expires; 661 this.userAgent = userAgent; 662 this.tokenURL = tokenURL; 663 this.baseURL = baseURL; 664 this.baseUploadURL = baseUploadURL; 665 this.autoRefresh = autoRefresh; 666 667 // Try to use deprecated value "maxRequestAttempts", else use newer value "maxRetryAttempts" 668 if (maxRequestAttempts > -1) { 669 this.maxRetryAttempts = maxRequestAttempts - 1; 670 } else { 671 this.maxRetryAttempts = maxRetryAttempts; 672 } 673 674 } 675 676 /** 677 * Notifies a refresh event to all the listeners. 678 */ 679 protected void notifyRefresh() { 680 for (BoxAPIConnectionListener listener : this.listeners) { 681 listener.onRefresh(this); 682 } 683 } 684 685 /** 686 * Notifies an error event to all the listeners. 687 * @param error A BoxAPIException instance. 688 */ 689 protected void notifyError(BoxAPIException error) { 690 for (BoxAPIConnectionListener listener : this.listeners) { 691 listener.onError(this, error); 692 } 693 } 694 695 /** 696 * Add a listener to listen to Box API connection events. 697 * @param listener a listener to listen to Box API connection. 698 */ 699 public void addListener(BoxAPIConnectionListener listener) { 700 this.listeners.add(listener); 701 } 702 703 /** 704 * Remove a listener listening to Box API connection events. 705 * @param listener the listener to remove. 706 */ 707 public void removeListener(BoxAPIConnectionListener listener) { 708 this.listeners.remove(listener); 709 } 710 711 /** 712 * Gets the RequestInterceptor associated with this API connection. 713 * @return the RequestInterceptor associated with this API connection. 714 */ 715 public RequestInterceptor getRequestInterceptor() { 716 return this.interceptor; 717 } 718 719 /** 720 * Sets a RequestInterceptor that can intercept requests and manipulate them before they're sent to the Box API. 721 * @param interceptor the RequestInterceptor. 722 */ 723 public void setRequestInterceptor(RequestInterceptor interceptor) { 724 this.interceptor = interceptor; 725 } 726 727 /** 728 * Get a lower-scoped token restricted to a resource for the list of scopes that are passed. 729 * @param scopes the list of scopes to which the new token should be restricted for 730 * @param resource the resource for which the new token has to be obtained 731 * @return scopedToken which has access token and other details 732 */ 733 public ScopedToken getLowerScopedToken(List<String> scopes, String resource) { 734 assert (scopes != null); 735 assert (scopes.size() > 0); 736 URL url = null; 737 try { 738 url = new URL(this.getTokenURL()); 739 } catch (MalformedURLException e) { 740 assert false : "An invalid refresh URL indicates a bug in the SDK."; 741 throw new RuntimeException("An invalid refresh URL indicates a bug in the SDK.", e); 742 } 743 744 StringBuilder spaceSeparatedScopes = new StringBuilder(); 745 for (int i = 0; i < scopes.size(); i++) { 746 spaceSeparatedScopes.append(scopes.get(i)); 747 if (i < scopes.size() - 1) { 748 spaceSeparatedScopes.append(" "); 749 } 750 } 751 752 String urlParameters = null; 753 754 if (resource != null) { 755 //this.getAccessToken() ensures we have a valid access token 756 urlParameters = String.format("grant_type=urn:ietf:params:oauth:grant-type:token-exchange" 757 + "&subject_token_type=urn:ietf:params:oauth:token-type:access_token&subject_token=%s" 758 + "&scope=%s&resource=%s", 759 this.getAccessToken(), spaceSeparatedScopes, resource); 760 } else { 761 //this.getAccessToken() ensures we have a valid access token 762 urlParameters = String.format("grant_type=urn:ietf:params:oauth:grant-type:token-exchange" 763 + "&subject_token_type=urn:ietf:params:oauth:token-type:access_token&subject_token=%s" 764 + "&scope=%s", 765 this.getAccessToken(), spaceSeparatedScopes); 766 } 767 768 BoxAPIRequest request = new BoxAPIRequest(this, url, "POST"); 769 request.shouldAuthenticate(false); 770 request.setBody(urlParameters); 771 772 String json; 773 try { 774 BoxJSONResponse response = (BoxJSONResponse) request.send(); 775 json = response.getJSON(); 776 } catch (BoxAPIException e) { 777 this.notifyError(e); 778 throw e; 779 } 780 781 JsonObject jsonObject = JsonObject.readFrom(json); 782 ScopedToken token = new ScopedToken(jsonObject); 783 token.setObtainedAt(System.currentTimeMillis()); 784 token.setExpiresIn(jsonObject.get("expires_in").asLong() * 1000); 785 return token; 786 } 787 788 /** 789 * Revokes the tokens associated with this API connection. This results in the connection no 790 * longer being able to make API calls until a fresh authorization is made by calling authenticate() 791 */ 792 public void revokeToken() { 793 794 URL url = null; 795 try { 796 url = new URL(this.revokeURL); 797 } catch (MalformedURLException e) { 798 assert false : "An invalid refresh URL indicates a bug in the SDK."; 799 throw new RuntimeException("An invalid refresh URL indicates a bug in the SDK.", e); 800 } 801 802 String urlParameters = String.format("token=%s&client_id=%s&client_secret=%s", 803 this.accessToken, this.clientID, this.clientSecret); 804 805 BoxAPIRequest request = new BoxAPIRequest(this, url, "POST"); 806 request.shouldAuthenticate(false); 807 request.setBody(urlParameters); 808 809 try { 810 request.send(); 811 } catch (BoxAPIException e) { 812 throw e; 813 } 814 } 815 816 /** 817 * Saves the state of this connection to a string so that it can be persisted and restored at a later time. 818 * 819 * <p>Note that proxy settings aren't automatically saved or restored. This is mainly due to security concerns 820 * around persisting proxy authentication details to the state string. If your connection uses a proxy, you will 821 * have to manually configure it again after restoring the connection.</p> 822 * 823 * @see #restore 824 * @return the state of this connection. 825 */ 826 public String save() { 827 JsonObject state = new JsonObject() 828 .add("accessToken", this.accessToken) 829 .add("refreshToken", this.refreshToken) 830 .add("lastRefresh", this.lastRefresh) 831 .add("expires", this.expires) 832 .add("userAgent", this.userAgent) 833 .add("tokenURL", this.tokenURL) 834 .add("baseURL", this.baseURL) 835 .add("baseUploadURL", this.baseUploadURL) 836 .add("autoRefresh", this.autoRefresh) 837 .add("maxRetryAttempts", this.maxRetryAttempts); 838 return state.toString(); 839 } 840 841 String lockAccessToken() { 842 if (this.autoRefresh && this.canRefresh() && this.needsRefresh()) { 843 this.refreshLock.writeLock().lock(); 844 try { 845 if (this.needsRefresh()) { 846 this.refresh(); 847 } 848 this.refreshLock.readLock().lock(); 849 } finally { 850 this.refreshLock.writeLock().unlock(); 851 } 852 } else { 853 this.refreshLock.readLock().lock(); 854 } 855 856 return this.accessToken; 857 } 858 859 void unlockAccessToken() { 860 this.refreshLock.readLock().unlock(); 861 } 862 863 /** 864 * Get the value for the X-Box-UA header. 865 * @return the header value. 866 */ 867 String getBoxUAHeader() { 868 869 return "agent=box-java-sdk/" + SDK_VERSION + "; env=Java/" + JAVA_VERSION; 870 } 871 872 /** 873 * Sets a custom header to be sent on all requests through this API connection. 874 * @param header the header name. 875 * @param value the header value. 876 */ 877 public void setCustomHeader(String header, String value) { 878 this.customHeaders.put(header, value); 879 } 880 881 /** 882 * Removes a custom header, so it will no longer be sent on requests through this API connection. 883 * @param header the header name. 884 */ 885 public void removeCustomHeader(String header) { 886 this.customHeaders.remove(header); 887 } 888 889 /** 890 * Suppresses email notifications from API actions. This is typically used by security or admin applications 891 * to prevent spamming end users when doing automated processing on their content. 892 */ 893 public void suppressNotifications() { 894 this.setCustomHeader(BOX_NOTIFICATIONS_HEADER, "off"); 895 } 896 897 /** 898 * Re-enable email notifications from API actions if they have been suppressed. 899 * @see #suppressNotifications 900 */ 901 public void enableNotifications() { 902 this.removeCustomHeader(BOX_NOTIFICATIONS_HEADER); 903 } 904 905 /** 906 * Set this API connection to make API calls on behalf of another users, impersonating them. This 907 * functionality can only be used by admins and service accounts. 908 * @param userID the ID of the user to act as. 909 */ 910 public void asUser(String userID) { 911 this.setCustomHeader(AS_USER_HEADER, userID); 912 } 913 914 /** 915 * Sets this API connection to make API calls on behalf of the user with whom the access token is associated. 916 * This undoes any previous calls to asUser(). 917 * @see #asUser 918 */ 919 public void asSelf() { 920 this.removeCustomHeader(AS_USER_HEADER); 921 } 922 923 Map<String, String> getHeaders() { 924 return this.customHeaders; 925 } 926}