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.List;
009import java.util.concurrent.locks.ReadWriteLock;
010import java.util.concurrent.locks.ReentrantReadWriteLock;
011
012import com.eclipsesource.json.JsonObject;
013
014/**
015 * Represents an authenticated connection to the Box API.
016 *
017 * <p>This class handles storing authentication information, automatic token refresh, and rate-limiting. It can also be
018 * used to configure the Box API endpoint URL in order to hit a different version of the API. Multiple instances of
019 * BoxAPIConnection may be created to support multi-user login.</p>
020 */
021public class BoxAPIConnection {
022    /**
023     * The default maximum number of times an API request will be tried when an error occurs.
024     */
025    public static final int DEFAULT_MAX_ATTEMPTS = 3;
026
027    private static final String AUTHORIZATION_URL = "https://account.box.com/api/oauth2/authorize";
028    private static final String TOKEN_URL_STRING = "https://api.box.com/oauth2/token";
029    private static final String DEFAULT_BASE_URL = "https://api.box.com/2.0/";
030    private static final String DEFAULT_BASE_UPLOAD_URL = "https://upload.box.com/api/2.0/";
031
032    /**
033     * The amount of buffer time, in milliseconds, to use when determining if an access token should be refreshed. For
034     * example, if REFRESH_EPSILON = 60000 and the access token expires in less than one minute, it will be refreshed.
035     */
036    private static final long REFRESH_EPSILON = 60000;
037
038    private final String clientID;
039    private final String clientSecret;
040    private final ReadWriteLock refreshLock;
041
042    // These volatile fields are used when determining if the access token needs to be refreshed. Since they are used in
043    // the double-checked lock in getAccessToken(), they must be atomic.
044    private volatile long lastRefresh;
045    private volatile long expires;
046
047    private Proxy proxy;
048    private String proxyUsername;
049    private String proxyPassword;
050
051    private String userAgent;
052    private String accessToken;
053    private String refreshToken;
054    private String tokenURL;
055    private String baseURL;
056    private String baseUploadURL;
057    private boolean autoRefresh;
058    private int maxRequestAttempts;
059    private List<BoxAPIConnectionListener> listeners;
060    private RequestInterceptor interceptor;
061
062    /**
063     * Constructs a new BoxAPIConnection that authenticates with a developer or access token.
064     * @param  accessToken a developer or access token to use for authenticating with the API.
065     */
066    public BoxAPIConnection(String accessToken) {
067        this(null, null, accessToken, null);
068    }
069
070    /**
071     * Constructs a new BoxAPIConnection with an access token that can be refreshed.
072     * @param  clientID     the client ID to use when refreshing the access token.
073     * @param  clientSecret the client secret to use when refreshing the access token.
074     * @param  accessToken  an initial access token to use for authenticating with the API.
075     * @param  refreshToken an initial refresh token to use when refreshing the access token.
076     */
077    public BoxAPIConnection(String clientID, String clientSecret, String accessToken, String refreshToken) {
078        this.clientID = clientID;
079        this.clientSecret = clientSecret;
080        this.accessToken = accessToken;
081        this.refreshToken = refreshToken;
082        this.tokenURL = TOKEN_URL_STRING;
083        this.baseURL = DEFAULT_BASE_URL;
084        this.baseUploadURL = DEFAULT_BASE_UPLOAD_URL;
085        this.autoRefresh = true;
086        this.maxRequestAttempts = DEFAULT_MAX_ATTEMPTS;
087        this.refreshLock = new ReentrantReadWriteLock();
088        this.userAgent = "Box Java SDK v2.8.1";
089        this.listeners = new ArrayList<BoxAPIConnectionListener>();
090    }
091
092    /**
093     * Constructs a new BoxAPIConnection with an auth code that was obtained from the first half of OAuth.
094     * @param  clientID     the client ID to use when exchanging the auth code for an access token.
095     * @param  clientSecret the client secret to use when exchanging the auth code for an access token.
096     * @param  authCode     an auth code obtained from the first half of the OAuth process.
097     */
098    public BoxAPIConnection(String clientID, String clientSecret, String authCode) {
099        this(clientID, clientSecret, null, null);
100        this.authenticate(authCode);
101    }
102
103    /**
104     * Constructs a new BoxAPIConnection.
105     * @param  clientID     the client ID to use when exchanging the auth code for an access token.
106     * @param  clientSecret the client secret to use when exchanging the auth code for an access token.
107     */
108    public BoxAPIConnection(String clientID, String clientSecret) {
109        this(clientID, clientSecret, null, null);
110    }
111
112    /**
113     * Constructs a new BoxAPIConnection levaraging BoxConfig.
114     * @param  boxConfig     BoxConfig file, which should have clientId and clientSecret
115     */
116    public BoxAPIConnection(BoxConfig boxConfig) {
117        this(boxConfig.getClientId(), boxConfig.getClientSecret(), null, null);
118    }
119
120    /**
121     * Restores a BoxAPIConnection from a saved state.
122     *
123     * @see    #save
124     * @param  clientID     the client ID to use with the connection.
125     * @param  clientSecret the client secret to use with the connection.
126     * @param  state        the saved state that was created with {@link #save}.
127     * @return              a restored API connection.
128     */
129    public static BoxAPIConnection restore(String clientID, String clientSecret, String state) {
130        BoxAPIConnection api = new BoxAPIConnection(clientID, clientSecret);
131        api.restore(state);
132        return api;
133    }
134
135    /**
136     * Return the authorization URL which is used to perform the authorization_code based OAuth2 flow.
137     * @param clientID the client ID to use with the connection.
138     * @param redirectUri the URL to which Box redirects the browser when authentication completes.
139     * @param state the text string that you choose.
140     *              Box sends the same string to your redirect URL when authentication is complete.
141     * @param scopes this optional parameter identifies the Box scopes available
142     *               to the application once it's authenticated.
143     * @return the authorization URL
144     */
145    public static URL getAuthorizationURL(String clientID, URI redirectUri, String state, List<String> scopes) {
146        URLTemplate template = new URLTemplate(AUTHORIZATION_URL);
147        QueryStringBuilder queryBuilder = new QueryStringBuilder().appendParam("client_id", clientID)
148                .appendParam("response_type", "code")
149                .appendParam("redirect_uri", redirectUri.toString())
150                .appendParam("state", state);
151
152        if (scopes != null && !scopes.isEmpty()) {
153            StringBuilder builder = new StringBuilder();
154            int size = scopes.size() - 1;
155            int i = 0;
156            while (i < size) {
157                builder.append(scopes.get(i));
158                builder.append(" ");
159                i++;
160            }
161            builder.append(scopes.get(i));
162
163            queryBuilder.appendParam("scope", builder.toString());
164        }
165
166        return template.buildWithQuery("", queryBuilder.toString());
167    }
168
169    /**
170     * Authenticates the API connection by obtaining access and refresh tokens using the auth code that was obtained
171     * from the first half of OAuth.
172     * @param authCode the auth code obtained from the first half of the OAuth process.
173     */
174    public void authenticate(String authCode) {
175        URL url = null;
176        try {
177            url = new URL(this.tokenURL);
178        } catch (MalformedURLException e) {
179            assert false : "An invalid token URL indicates a bug in the SDK.";
180            throw new RuntimeException("An invalid token URL indicates a bug in the SDK.", e);
181        }
182
183        String urlParameters = String.format("grant_type=authorization_code&code=%s&client_id=%s&client_secret=%s",
184            authCode, this.clientID, this.clientSecret);
185
186        BoxAPIRequest request = new BoxAPIRequest(this, url, "POST");
187        request.shouldAuthenticate(false);
188        request.setBody(urlParameters);
189
190        BoxJSONResponse response = (BoxJSONResponse) request.send();
191        String json = response.getJSON();
192
193        JsonObject jsonObject = JsonObject.readFrom(json);
194        this.accessToken = jsonObject.get("access_token").asString();
195        this.refreshToken = jsonObject.get("refresh_token").asString();
196        this.lastRefresh = System.currentTimeMillis();
197        this.expires = jsonObject.get("expires_in").asLong() * 1000;
198    }
199
200    /**
201     * Gets the client ID.
202     * @return the client ID.
203     */
204    public String getClientID() {
205        return this.clientID;
206    }
207
208    /**
209     * Gets the client secret.
210     * @return the client secret.
211     */
212    public String getClientSecret() {
213        return this.clientSecret;
214    }
215
216    /**
217     * Sets the amount of time for which this connection's access token is valid before it must be refreshed.
218     * @param milliseconds the number of milliseconds for which the access token is valid.
219     */
220    public void setExpires(long milliseconds) {
221        this.expires = milliseconds;
222    }
223
224    /**
225     * Gets the amount of time for which this connection's access token is valid.
226     * @return the amount of time in milliseconds.
227     */
228    public long getExpires() {
229        return this.expires;
230    }
231
232    /**
233     * Gets the token URL that's used to request access tokens.  The default value is
234     * "https://www.box.com/api/oauth2/token".
235     * @return the token URL.
236     */
237    public String getTokenURL() {
238        return this.tokenURL;
239    }
240
241    /**
242     * Sets the token URL that's used to request access tokens.  For example, the default token URL is
243     * "https://www.box.com/api/oauth2/token".
244     * @param tokenURL the token URL.
245     */
246    public void setTokenURL(String tokenURL) {
247        this.tokenURL = tokenURL;
248    }
249
250    /**
251     * Gets the base URL that's used when sending requests to the Box API. The default value is
252     * "https://api.box.com/2.0/".
253     * @return the base URL.
254     */
255    public String getBaseURL() {
256        return this.baseURL;
257    }
258
259    /**
260     * Sets the base URL to be used when sending requests to the Box API. For example, the default base URL is
261     * "https://api.box.com/2.0/".
262     * @param baseURL a base URL
263     */
264    public void setBaseURL(String baseURL) {
265        this.baseURL = baseURL;
266    }
267
268    /**
269     * Gets the base upload URL that's used when performing file uploads to Box.
270     * @return the base upload URL.
271     */
272    public String getBaseUploadURL() {
273        return this.baseUploadURL;
274    }
275
276    /**
277     * Sets the base upload URL to be used when performing file uploads to Box.
278     * @param baseUploadURL a base upload URL.
279     */
280    public void setBaseUploadURL(String baseUploadURL) {
281        this.baseUploadURL = baseUploadURL;
282    }
283
284    /**
285     * Gets the user agent that's used when sending requests to the Box API.
286     * @return the user agent.
287     */
288    public String getUserAgent() {
289        return this.userAgent;
290    }
291
292    /**
293     * Sets the user agent to be used when sending requests to the Box API.
294     * @param userAgent the user agent.
295     */
296    public void setUserAgent(String userAgent) {
297        this.userAgent = userAgent;
298    }
299
300    /**
301     * Gets an access token that can be used to authenticate an API request. This method will automatically refresh the
302     * access token if it has expired since the last call to <code>getAccessToken()</code>.
303     * @return a valid access token that can be used to authenticate an API request.
304     */
305    public String getAccessToken() {
306        if (this.autoRefresh && this.canRefresh() && this.needsRefresh()) {
307            this.refreshLock.writeLock().lock();
308            try {
309                if (this.needsRefresh()) {
310                    this.refresh();
311                }
312            } finally {
313                this.refreshLock.writeLock().unlock();
314            }
315        }
316
317        return this.accessToken;
318    }
319
320    /**
321     * Sets the access token to use when authenticating API requests.
322     * @param accessToken a valid access token to use when authenticating API requests.
323     */
324    public void setAccessToken(String accessToken) {
325        this.accessToken = accessToken;
326    }
327
328    /**
329     * Gets the refresh lock to be used when refreshing an access token.
330     * @return the refresh lock.
331     */
332    protected ReadWriteLock getRefreshLock() {
333        return this.refreshLock;
334    }
335    /**
336     * Gets a refresh token that can be used to refresh an access token.
337     * @return a valid refresh token.
338     */
339    public String getRefreshToken() {
340        return this.refreshToken;
341    }
342
343    /**
344     * Sets the refresh token to use when refreshing an access token.
345     * @param refreshToken a valid refresh token.
346     */
347    public void setRefreshToken(String refreshToken) {
348        this.refreshToken = refreshToken;
349    }
350
351    /**
352     * Gets the last time that the access token was refreshed.
353     *
354     * @return the last refresh time in milliseconds.
355     */
356    public long getLastRefresh() {
357        return this.lastRefresh;
358    }
359
360    /**
361     * Sets the last time that the access token was refreshed.
362     *
363     * <p>This value is used when determining if an access token needs to be auto-refreshed. If the amount of time since
364     * the last refresh exceeds the access token's expiration time, then the access token will be refreshed.</p>
365     *
366     * @param lastRefresh the new last refresh time in milliseconds.
367     */
368    public void setLastRefresh(long lastRefresh) {
369        this.lastRefresh = lastRefresh;
370    }
371
372    /**
373     * Enables or disables automatic refreshing of this connection's access token. Defaults to true.
374     * @param autoRefresh true to enable auto token refresh; otherwise false.
375     */
376    public void setAutoRefresh(boolean autoRefresh) {
377        this.autoRefresh = autoRefresh;
378    }
379
380    /**
381     * Gets whether or not automatic refreshing of this connection's access token is enabled. Defaults to true.
382     * @return true if auto token refresh is enabled; otherwise false.
383     */
384    public boolean getAutoRefresh() {
385        return this.autoRefresh;
386    }
387
388    /**
389     * Gets the maximum number of times an API request will be tried when an error occurs.
390     * @return the maximum number of request attempts.
391     */
392    public int getMaxRequestAttempts() {
393        return this.maxRequestAttempts;
394    }
395
396    /**
397     * Sets the maximum number of times an API request will be tried when an error occurs.
398     * @param attempts the maximum number of request attempts.
399     */
400    public void setMaxRequestAttempts(int attempts) {
401        this.maxRequestAttempts = attempts;
402    }
403
404    /**
405     * Gets the proxy value to use for API calls to Box.
406     * @return the current proxy.
407     */
408    public Proxy getProxy() {
409        return this.proxy;
410    }
411
412    /**
413     * Sets the proxy to use for API calls to Box.
414     * @param proxy the proxy to use for API calls to Box.
415     */
416    public void setProxy(Proxy proxy) {
417        this.proxy = proxy;
418    }
419
420    /**
421     * Gets the username to use for a proxy that requires basic auth.
422     * @return the username to use for a proxy that requires basic auth.
423     */
424    public String getProxyUsername() {
425        return this.proxyUsername;
426    }
427
428    /**
429     * Sets the username to use for a proxy that requires basic auth.
430     * @param proxyUsername the username to use for a proxy that requires basic auth.
431     */
432    public void setProxyUsername(String proxyUsername) {
433        this.proxyUsername = proxyUsername;
434    }
435
436    /**
437     * Gets the password to use for a proxy that requires basic auth.
438     * @return the password to use for a proxy that requires basic auth.
439     */
440    public String getProxyPassword() {
441        return this.proxyPassword;
442    }
443
444    /**
445     * Sets the password to use for a proxy that requires basic auth.
446     * @param proxyPassword the password to use for a proxy that requires basic auth.
447     */
448    public void setProxyPassword(String proxyPassword) {
449        this.proxyPassword = proxyPassword;
450    }
451
452    /**
453     * Determines if this connection's access token can be refreshed. An access token cannot be refreshed if a refresh
454     * token was never set.
455     * @return true if the access token can be refreshed; otherwise false.
456     */
457    public boolean canRefresh() {
458        return this.refreshToken != null;
459    }
460
461    /**
462     * Determines if this connection's access token has expired and needs to be refreshed.
463     * @return true if the access token needs to be refreshed; otherwise false.
464     */
465    public boolean needsRefresh() {
466        boolean needsRefresh;
467
468        this.refreshLock.readLock().lock();
469        long now = System.currentTimeMillis();
470        long tokenDuration = (now - this.lastRefresh);
471        needsRefresh = (tokenDuration >= this.expires - REFRESH_EPSILON);
472        this.refreshLock.readLock().unlock();
473
474        return needsRefresh;
475    }
476
477    /**
478     * Refresh's this connection's access token using its refresh token.
479     * @throws IllegalStateException if this connection's access token cannot be refreshed.
480     */
481    public void refresh() {
482        this.refreshLock.writeLock().lock();
483
484        if (!this.canRefresh()) {
485            this.refreshLock.writeLock().unlock();
486            throw new IllegalStateException("The BoxAPIConnection cannot be refreshed because it doesn't have a "
487                + "refresh token.");
488        }
489
490        URL url = null;
491        try {
492            url = new URL(this.tokenURL);
493        } catch (MalformedURLException e) {
494            this.refreshLock.writeLock().unlock();
495            assert false : "An invalid refresh URL indicates a bug in the SDK.";
496            throw new RuntimeException("An invalid refresh URL indicates a bug in the SDK.", e);
497        }
498
499        String urlParameters = String.format("grant_type=refresh_token&refresh_token=%s&client_id=%s&client_secret=%s",
500            this.refreshToken, this.clientID, this.clientSecret);
501
502        BoxAPIRequest request = new BoxAPIRequest(this, url, "POST");
503        request.shouldAuthenticate(false);
504        request.setBody(urlParameters);
505
506        String json;
507        try {
508            BoxJSONResponse response = (BoxJSONResponse) request.send();
509            json = response.getJSON();
510        } catch (BoxAPIException e) {
511            this.notifyError(e);
512            this.refreshLock.writeLock().unlock();
513            throw e;
514        }
515
516        JsonObject jsonObject = JsonObject.readFrom(json);
517        this.accessToken = jsonObject.get("access_token").asString();
518        this.refreshToken = jsonObject.get("refresh_token").asString();
519        this.lastRefresh = System.currentTimeMillis();
520        this.expires = jsonObject.get("expires_in").asLong() * 1000;
521
522        this.notifyRefresh();
523
524        this.refreshLock.writeLock().unlock();
525    }
526
527    /**
528     * Restores a saved connection state into this BoxAPIConnection.
529     *
530     * @see    #save
531     * @param  state the saved state that was created with {@link #save}.
532     */
533    public void restore(String state) {
534        JsonObject json = JsonObject.readFrom(state);
535        String accessToken = json.get("accessToken").asString();
536        String refreshToken = json.get("refreshToken").asString();
537        long lastRefresh = json.get("lastRefresh").asLong();
538        long expires = json.get("expires").asLong();
539        String userAgent = json.get("userAgent").asString();
540        String tokenURL = json.get("tokenURL").asString();
541        String baseURL = json.get("baseURL").asString();
542        String baseUploadURL = json.get("baseUploadURL").asString();
543        boolean autoRefresh = json.get("autoRefresh").asBoolean();
544        int maxRequestAttempts = json.get("maxRequestAttempts").asInt();
545
546        this.accessToken = accessToken;
547        this.refreshToken = refreshToken;
548        this.lastRefresh = lastRefresh;
549        this.expires = expires;
550        this.userAgent = userAgent;
551        this.tokenURL = tokenURL;
552        this.baseURL = baseURL;
553        this.baseUploadURL = baseUploadURL;
554        this.autoRefresh = autoRefresh;
555        this.maxRequestAttempts = maxRequestAttempts;
556    }
557
558    /**
559     * Notifies a refresh event to all the listeners.
560     */
561    protected void notifyRefresh() {
562        for (BoxAPIConnectionListener listener : this.listeners) {
563            listener.onRefresh(this);
564        }
565    }
566
567    /**
568     * Notifies an error event to all the listeners.
569     * @param error A BoxAPIException instance.
570     */
571    protected void notifyError(BoxAPIException error) {
572        for (BoxAPIConnectionListener listener : this.listeners) {
573            listener.onError(this, error);
574        }
575    }
576
577    /**
578     * Add a listener to listen to Box API connection events.
579     * @param listener a listener to listen to Box API connection.
580     */
581    public void addListener(BoxAPIConnectionListener listener) {
582        this.listeners.add(listener);
583    }
584
585    /**
586     * Remove a listener listening to Box API connection events.
587     * @param listener the listener to remove.
588     */
589    public void removeListener(BoxAPIConnectionListener listener) {
590        this.listeners.remove(listener);
591    }
592
593    /**
594     * Gets the RequestInterceptor associated with this API connection.
595     * @return the RequestInterceptor associated with this API connection.
596     */
597    public RequestInterceptor getRequestInterceptor() {
598        return this.interceptor;
599    }
600
601    /**
602     * Sets a RequestInterceptor that can intercept requests and manipulate them before they're sent to the Box API.
603     * @param interceptor the RequestInterceptor.
604     */
605    public void setRequestInterceptor(RequestInterceptor interceptor) {
606        this.interceptor = interceptor;
607    }
608
609    /**
610     * Get a lower-scoped token restricted to a resource for the list of scopes that are passed.
611     * @param scopes the list of scopes to which the new token should be restricted for
612     * @param resource the resource for which the new token has to be obtained
613     * @return scopedToken which has access token and other details
614     */
615    public ScopedToken getLowerScopedToken(List<String> scopes, String resource) {
616        assert (scopes != null);
617        assert (scopes.size() > 0);
618        URL url = null;
619        try {
620            url = new URL(this.getTokenURL());
621        } catch (MalformedURLException e) {
622            assert false : "An invalid refresh URL indicates a bug in the SDK.";
623            throw new RuntimeException("An invalid refresh URL indicates a bug in the SDK.", e);
624        }
625
626        StringBuilder spaceSeparatedScopes = new StringBuilder();
627        for (int i = 0; i < scopes.size(); i++) {
628            spaceSeparatedScopes.append(scopes.get(i));
629            if (i < scopes.size() - 1) {
630                spaceSeparatedScopes.append(" ");
631            }
632        }
633
634        String urlParameters = null;
635
636        if (resource != null) {
637            //this.getAccessToken() ensures we have a valid access token
638            urlParameters = String.format("grant_type=urn:ietf:params:oauth:grant-type:token-exchange"
639                    + "&subject_token_type=urn:ietf:params:oauth:token-type:access_token&subject_token=%s"
640                    + "&scope=%s&resource=%s",
641                this.getAccessToken(), spaceSeparatedScopes, resource);
642        } else {
643            //this.getAccessToken() ensures we have a valid access token
644            urlParameters = String.format("grant_type=urn:ietf:params:oauth:grant-type:token-exchange"
645                    + "&subject_token_type=urn:ietf:params:oauth:token-type:access_token&subject_token=%s"
646                    + "&scope=%s",
647                this.getAccessToken(), spaceSeparatedScopes);
648        }
649
650        BoxAPIRequest request = new BoxAPIRequest(this, url, "POST");
651        request.shouldAuthenticate(false);
652        request.setBody(urlParameters);
653
654        String json;
655        try {
656            BoxJSONResponse response = (BoxJSONResponse) request.send();
657            json = response.getJSON();
658        } catch (BoxAPIException e) {
659            this.notifyError(e);
660            throw e;
661        }
662
663        JsonObject jsonObject = JsonObject.readFrom(json);
664        ScopedToken token = new ScopedToken(jsonObject);
665        token.setObtainedAt(System.currentTimeMillis());
666        token.setExpiresIn(jsonObject.get("expires_in").asLong() * 1000);
667        return token;
668    }
669
670    /**
671     * Saves the state of this connection to a string so that it can be persisted and restored at a later time.
672     *
673     * <p>Note that proxy settings aren't automatically saved or restored. This is mainly due to security concerns
674     * around persisting proxy authentication details to the state string. If your connection uses a proxy, you will
675     * have to manually configure it again after restoring the connection.</p>
676     *
677     * @see    #restore
678     * @return the state of this connection.
679     */
680    public String save() {
681        JsonObject state = new JsonObject()
682            .add("accessToken", this.accessToken)
683            .add("refreshToken", this.refreshToken)
684            .add("lastRefresh", this.lastRefresh)
685            .add("expires", this.expires)
686            .add("userAgent", this.userAgent)
687            .add("tokenURL", this.tokenURL)
688            .add("baseURL", this.baseURL)
689            .add("baseUploadURL", this.baseUploadURL)
690            .add("autoRefresh", this.autoRefresh)
691            .add("maxRequestAttempts", this.maxRequestAttempts);
692        return state.toString();
693    }
694
695    String lockAccessToken() {
696        if (this.autoRefresh && this.canRefresh() && this.needsRefresh()) {
697            this.refreshLock.writeLock().lock();
698            try {
699                if (this.needsRefresh()) {
700                    this.refresh();
701                }
702                this.refreshLock.readLock().lock();
703            } finally {
704                this.refreshLock.writeLock().unlock();
705            }
706        } else {
707            this.refreshLock.readLock().lock();
708        }
709
710        return this.accessToken;
711    }
712
713    void unlockAccessToken() {
714        this.refreshLock.readLock().unlock();
715    }
716}