• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2015, Google Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions are
6  * met:
7  *
8  *    * Redistributions of source code must retain the above copyright
9  * notice, this list of conditions and the following disclaimer.
10  *    * Redistributions in binary form must reproduce the above
11  * copyright notice, this list of conditions and the following disclaimer
12  * in the documentation and/or other materials provided with the
13  * distribution.
14  *
15  *    * Neither the name of Google Inc. nor the names of its
16  * contributors may be used to endorse or promote products derived from
17  * this software without specific prior written permission.
18  *
19  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30  */
31 
32 package com.google.auth.oauth2;
33 
34 import com.google.api.client.util.Clock;
35 import com.google.auth.Credentials;
36 import com.google.auth.RequestMetadataCallback;
37 import com.google.auth.http.AuthHttpConstants;
38 import com.google.common.annotations.VisibleForTesting;
39 import com.google.common.base.MoreObjects;
40 import com.google.common.base.Preconditions;
41 import com.google.common.collect.ImmutableList;
42 import com.google.common.collect.ImmutableMap;
43 import com.google.common.collect.Iterables;
44 import com.google.common.util.concurrent.AbstractFuture;
45 import com.google.common.util.concurrent.FutureCallback;
46 import com.google.common.util.concurrent.Futures;
47 import com.google.common.util.concurrent.ListenableFuture;
48 import com.google.common.util.concurrent.ListenableFutureTask;
49 import com.google.common.util.concurrent.MoreExecutors;
50 import com.google.errorprone.annotations.CanIgnoreReturnValue;
51 import java.io.IOException;
52 import java.io.ObjectInputStream;
53 import java.io.Serializable;
54 import java.net.URI;
55 import java.time.Duration;
56 import java.util.ArrayList;
57 import java.util.Date;
58 import java.util.List;
59 import java.util.Map;
60 import java.util.Objects;
61 import java.util.ServiceLoader;
62 import java.util.concurrent.Callable;
63 import java.util.concurrent.ExecutionException;
64 import java.util.concurrent.Executor;
65 import javax.annotation.Nullable;
66 
67 /** Base type for Credentials using OAuth2. */
68 public class OAuth2Credentials extends Credentials {
69 
70   private static final long serialVersionUID = 4556936364828217687L;
71   static final Duration DEFAULT_EXPIRATION_MARGIN = Duration.ofMinutes(3);
72   static final Duration DEFAULT_REFRESH_MARGIN = Duration.ofMinutes(3).plusSeconds(45);
73   private static final ImmutableMap<String, List<String>> EMPTY_EXTRA_HEADERS = ImmutableMap.of();
74 
75   @VisibleForTesting private final Duration expirationMargin;
76   @VisibleForTesting private final Duration refreshMargin;
77 
78   // byte[] is serializable, so the lock variable can be final
79   @VisibleForTesting final Object lock = new byte[0];
80   private volatile OAuthValue value = null;
81   @VisibleForTesting transient RefreshTask refreshTask;
82 
83   // Change listeners are not serialized
84   private transient List<CredentialsChangedListener> changeListeners;
85   // Until we expose this to the users it can remain transient and non-serializable
86   @VisibleForTesting transient Clock clock = Clock.SYSTEM;
87 
88   /**
89    * Returns the credentials instance from the given access token.
90    *
91    * @param accessToken the access token
92    * @return the credentials instance
93    */
create(AccessToken accessToken)94   public static OAuth2Credentials create(AccessToken accessToken) {
95     return OAuth2Credentials.newBuilder().setAccessToken(accessToken).build();
96   }
97 
98   /** Default constructor. */
OAuth2Credentials()99   protected OAuth2Credentials() {
100     this(null);
101   }
102 
103   /**
104    * Constructor with explicit access token.
105    *
106    * @param accessToken initial or temporary access token
107    */
OAuth2Credentials(AccessToken accessToken)108   protected OAuth2Credentials(AccessToken accessToken) {
109     this(accessToken, DEFAULT_REFRESH_MARGIN, DEFAULT_EXPIRATION_MARGIN);
110   }
111 
OAuth2Credentials( AccessToken accessToken, Duration refreshMargin, Duration expirationMargin)112   protected OAuth2Credentials(
113       AccessToken accessToken, Duration refreshMargin, Duration expirationMargin) {
114     if (accessToken != null) {
115       this.value = OAuthValue.create(accessToken, EMPTY_EXTRA_HEADERS);
116     }
117 
118     this.refreshMargin = Preconditions.checkNotNull(refreshMargin, "refreshMargin");
119     Preconditions.checkArgument(!refreshMargin.isNegative(), "refreshMargin can't be negative");
120     this.expirationMargin = Preconditions.checkNotNull(expirationMargin, "expirationMargin");
121     Preconditions.checkArgument(
122         !expirationMargin.isNegative(), "expirationMargin can't be negative");
123   }
124 
125   @Override
getAuthenticationType()126   public String getAuthenticationType() {
127     return "OAuth2";
128   }
129 
130   @Override
hasRequestMetadata()131   public boolean hasRequestMetadata() {
132     return true;
133   }
134 
135   @Override
hasRequestMetadataOnly()136   public boolean hasRequestMetadataOnly() {
137     return true;
138   }
139 
140   /**
141    * Returns the cached access token.
142    *
143    * <p>If not set, you should call {@link #refresh()} to fetch and cache an access token.
144    *
145    * @return The cached access token.
146    */
getAccessToken()147   public final AccessToken getAccessToken() {
148     OAuthValue localState = value;
149     if (localState != null) {
150       return localState.temporaryAccess;
151     }
152     return null;
153   }
154 
155   /** Returns the credentials' refresh margin. */
156   @VisibleForTesting
getRefreshMargin()157   Duration getRefreshMargin() {
158     return this.refreshMargin;
159   }
160 
161   /** Returns the credentials' expiration margin. */
162   @VisibleForTesting
getExpirationMargin()163   Duration getExpirationMargin() {
164     return this.expirationMargin;
165   }
166 
167   @Override
getRequestMetadata( final URI uri, Executor executor, final RequestMetadataCallback callback)168   public void getRequestMetadata(
169       final URI uri, Executor executor, final RequestMetadataCallback callback) {
170 
171     Futures.addCallback(
172         asyncFetch(executor),
173         new FutureCallbackToMetadataCallbackAdapter(callback),
174         MoreExecutors.directExecutor());
175   }
176 
177   /**
178    * Provide the request metadata by ensuring there is a current access token and providing it as an
179    * authorization bearer token.
180    */
181   @Override
getRequestMetadata(URI uri)182   public Map<String, List<String>> getRequestMetadata(URI uri) throws IOException {
183     return unwrapDirectFuture(asyncFetch(MoreExecutors.directExecutor())).requestMetadata;
184   }
185 
186   /**
187    * Request a new token regardless of the current token state. If the current token is not expired,
188    * it will still be returned during the refresh.
189    */
190   @Override
refresh()191   public void refresh() throws IOException {
192     AsyncRefreshResult refreshResult = getOrCreateRefreshTask();
193     refreshResult.executeIfNew(MoreExecutors.directExecutor());
194     unwrapDirectFuture(refreshResult.task);
195   }
196 
197   /**
198    * Refresh these credentials only if they have expired or are expiring imminently.
199    *
200    * @throws IOException during token refresh.
201    */
refreshIfExpired()202   public void refreshIfExpired() throws IOException {
203     // asyncFetch will ensure that the token is refreshed
204     unwrapDirectFuture(asyncFetch(MoreExecutors.directExecutor()));
205   }
206 
207   /**
208    * Attempts to get a fresh token.
209    *
210    * <p>If a fresh token is already available, it will be immediately returned. Otherwise a refresh
211    * will be scheduled using the passed in executor. While a token is being freshed, a stale value
212    * will be returned.
213    */
asyncFetch(Executor executor)214   private ListenableFuture<OAuthValue> asyncFetch(Executor executor) {
215     AsyncRefreshResult refreshResult = null;
216 
217     // fast and common path: skip the lock if the token is fresh
218     // The inherent race condition here is a non-issue: even if the value gets replaced after the
219     // state check, the new token will still be fresh.
220     if (getState() == CacheState.FRESH) {
221       return Futures.immediateFuture(value);
222     }
223 
224     // Schedule a refresh as necessary
225     synchronized (lock) {
226       if (getState() != CacheState.FRESH) {
227         refreshResult = getOrCreateRefreshTask();
228       }
229     }
230     // Execute the refresh if necessary. This should be done outside of the lock to avoid blocking
231     // metadata requests during a stale refresh.
232     if (refreshResult != null) {
233       refreshResult.executeIfNew(executor);
234     }
235 
236     synchronized (lock) {
237       // Immediately resolve the token token if its not expired, or wait for the refresh task to
238       // complete
239       if (getState() != CacheState.EXPIRED) {
240         return Futures.immediateFuture(value);
241       } else if (refreshResult != null) {
242         return refreshResult.task;
243       } else {
244         // Should never happen
245         return Futures.immediateFailedFuture(
246             new IllegalStateException("Credentials expired, but there is no task to refresh"));
247       }
248     }
249   }
250 
251   /**
252    * Atomically creates a single flight refresh token task.
253    *
254    * <p>Only a single refresh task can be scheduled at a time. If there is an existing task, it will
255    * be returned for subsequent invocations. However if a new task is created, it is the
256    * responsibility of the caller to execute it. The task will clear the single flight slow upon
257    * completion.
258    */
getOrCreateRefreshTask()259   private AsyncRefreshResult getOrCreateRefreshTask() {
260     synchronized (lock) {
261       if (refreshTask != null) {
262         return new AsyncRefreshResult(refreshTask, false);
263       }
264 
265       final ListenableFutureTask<OAuthValue> task =
266           ListenableFutureTask.create(
267               new Callable<OAuthValue>() {
268                 @Override
269                 public OAuthValue call() throws Exception {
270                   return OAuthValue.create(refreshAccessToken(), getAdditionalHeaders());
271                 }
272               });
273 
274       refreshTask = new RefreshTask(task, new RefreshTaskListener(task));
275 
276       return new AsyncRefreshResult(refreshTask, true);
277     }
278   }
279 
280   /**
281    * Async callback for committing the result from a token refresh.
282    *
283    * <p>The result will be stored, listeners are invoked and the single flight slot is cleared.
284    */
finishRefreshAsync(ListenableFuture<OAuthValue> finishedTask)285   private void finishRefreshAsync(ListenableFuture<OAuthValue> finishedTask) {
286     synchronized (lock) {
287       try {
288         this.value = Futures.getDone(finishedTask);
289         for (CredentialsChangedListener listener : changeListeners) {
290           listener.onChanged(this);
291         }
292       } catch (Exception e) {
293         // noop
294       } finally {
295         if (this.refreshTask != null && this.refreshTask.getTask() == finishedTask) {
296           this.refreshTask = null;
297         }
298       }
299     }
300   }
301 
302   /**
303    * Unwraps the value from the future.
304    *
305    * <p>Under most circumstances, the underlying future will already be resolved by the
306    * DirectExecutor. In those cases, the error stacktraces will be rooted in the caller's call tree.
307    * However, in some cases when async and sync usage is mixed, it's possible that a blocking call
308    * will await an async future. In those cases, the stacktrace will be orphaned and be rooted in a
309    * thread of whatever executor the async call used. This doesn't affect correctness and is
310    * extremely unlikely.
311    */
unwrapDirectFuture(ListenableFuture<T> future)312   private static <T> T unwrapDirectFuture(ListenableFuture<T> future) throws IOException {
313     try {
314       return future.get();
315     } catch (InterruptedException e) {
316       Thread.currentThread().interrupt();
317       throw new IOException("Interrupted while asynchronously refreshing the access token", e);
318     } catch (ExecutionException e) {
319       Throwable cause = e.getCause();
320       if (cause instanceof IOException) {
321         throw (IOException) cause;
322       } else if (cause instanceof RuntimeException) {
323         throw (RuntimeException) cause;
324       } else {
325         throw new IOException("Unexpected error refreshing access token", cause);
326       }
327     }
328   }
329 
330   /** Computes the effective credential state in relation to the current time. */
getState()331   private CacheState getState() {
332     OAuthValue localValue = value;
333 
334     if (localValue == null) {
335       return CacheState.EXPIRED;
336     }
337     Date expirationTime = localValue.temporaryAccess.getExpirationTime();
338 
339     if (expirationTime == null) {
340       return CacheState.FRESH;
341     }
342 
343     Duration remaining = Duration.ofMillis(expirationTime.getTime() - clock.currentTimeMillis());
344     if (remaining.compareTo(expirationMargin) <= 0) {
345       return CacheState.EXPIRED;
346     }
347 
348     if (remaining.compareTo(refreshMargin) <= 0) {
349       return CacheState.STALE;
350     }
351 
352     return CacheState.FRESH;
353   }
354 
355   /**
356    * Method to refresh the access token according to the specific type of credentials.
357    *
358    * <p>Throws IllegalStateException if not overridden since direct use of OAuth2Credentials is only
359    * for temporary or non-refreshing access tokens.
360    *
361    * @return never
362    * @throws IllegalStateException always. OAuth2Credentials does not support refreshing the access
363    *     token. An instance with a new access token or a derived type that supports refreshing
364    *     should be used instead.
365    */
refreshAccessToken()366   public AccessToken refreshAccessToken() throws IOException {
367     throw new IllegalStateException(
368         "OAuth2Credentials instance does not support refreshing the"
369             + " access token. An instance with a new access token should be used, or a derived type"
370             + " that supports refreshing.");
371   }
372 
373   /**
374    * Provide additional headers to return as request metadata.
375    *
376    * @return additional headers
377    */
getAdditionalHeaders()378   protected Map<String, List<String>> getAdditionalHeaders() {
379     return EMPTY_EXTRA_HEADERS;
380   }
381 
382   /**
383    * Adds a listener that is notified when the Credentials data changes.
384    *
385    * <p>This is called when token content changes, such as when the access token is refreshed. This
386    * is typically used by code caching the access token.
387    *
388    * @param listener the listener to be added
389    */
addChangeListener(CredentialsChangedListener listener)390   public final void addChangeListener(CredentialsChangedListener listener) {
391     synchronized (lock) {
392       if (changeListeners == null) {
393         changeListeners = new ArrayList<>();
394       }
395       changeListeners.add(listener);
396     }
397   }
398 
399   /**
400    * Removes a listener that was added previously.
401    *
402    * @param listener The listener to be removed.
403    */
removeChangeListener(CredentialsChangedListener listener)404   public final void removeChangeListener(CredentialsChangedListener listener) {
405     synchronized (lock) {
406       if (changeListeners != null) {
407         changeListeners.remove(listener);
408       }
409     }
410   }
411 
412   /**
413    * Listener for changes to credentials.
414    *
415    * <p>This is called when token content changes, such as when the access token is refreshed. This
416    * is typically used by code caching the access token.
417    */
418   public interface CredentialsChangedListener {
419 
420     /**
421      * Notifies that the credentials have changed.
422      *
423      * <p>This is called when token content changes, such as when the access token is refreshed.
424      * This is typically used by code caching the access token.
425      *
426      * @param credentials The updated credentials instance
427      * @throws IOException My be thrown by listeners if saving credentials fails.
428      */
onChanged(OAuth2Credentials credentials)429     void onChanged(OAuth2Credentials credentials) throws IOException;
430   }
431 
432   @Override
hashCode()433   public int hashCode() {
434     return Objects.hashCode(value);
435   }
436 
437   @Nullable
getRequestMetadataInternal()438   protected Map<String, List<String>> getRequestMetadataInternal() {
439     OAuthValue localValue = value;
440     if (localValue != null) {
441       return localValue.requestMetadata;
442     }
443     return null;
444   }
445 
446   @Override
toString()447   public String toString() {
448     OAuthValue localValue = value;
449 
450     Map<String, List<String>> requestMetadata = null;
451     AccessToken temporaryAccess = null;
452 
453     if (localValue != null) {
454       requestMetadata = localValue.requestMetadata;
455       temporaryAccess = localValue.temporaryAccess;
456     }
457     return MoreObjects.toStringHelper(this)
458         .add("requestMetadata", requestMetadata)
459         .add("temporaryAccess", temporaryAccess)
460         .toString();
461   }
462 
463   @Override
equals(Object obj)464   public boolean equals(Object obj) {
465     if (!(obj instanceof OAuth2Credentials)) {
466       return false;
467     }
468     OAuth2Credentials other = (OAuth2Credentials) obj;
469     return Objects.equals(this.value, other.value);
470   }
471 
readObject(ObjectInputStream input)472   private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
473     input.defaultReadObject();
474     clock = Clock.SYSTEM;
475     refreshTask = null;
476   }
477 
478   @SuppressWarnings("unchecked")
newInstance(String className)479   protected static <T> T newInstance(String className) throws IOException, ClassNotFoundException {
480     try {
481       return (T) Class.forName(className).newInstance();
482     } catch (InstantiationException | IllegalAccessException e) {
483       throw new IOException(e);
484     }
485   }
486 
getFromServiceLoader(Class<? extends T> clazz, T defaultInstance)487   protected static <T> T getFromServiceLoader(Class<? extends T> clazz, T defaultInstance) {
488     return Iterables.getFirst(ServiceLoader.load(clazz), defaultInstance);
489   }
490 
newBuilder()491   public static Builder newBuilder() {
492     return new Builder();
493   }
494 
toBuilder()495   public Builder toBuilder() {
496     return new Builder(this);
497   }
498 
499   /** Stores an immutable snapshot of the accesstoken owned by {@link OAuth2Credentials} */
500   static class OAuthValue implements Serializable {
501     private final AccessToken temporaryAccess;
502     private final Map<String, List<String>> requestMetadata;
503 
create(AccessToken token, Map<String, List<String>> additionalHeaders)504     static OAuthValue create(AccessToken token, Map<String, List<String>> additionalHeaders) {
505       return new OAuthValue(
506           token,
507           ImmutableMap.<String, List<String>>builder()
508               .put(
509                   AuthHttpConstants.AUTHORIZATION,
510                   ImmutableList.of(OAuth2Utils.BEARER_PREFIX + token.getTokenValue()))
511               .putAll(additionalHeaders)
512               .build());
513     }
514 
OAuthValue(AccessToken temporaryAccess, Map<String, List<String>> requestMetadata)515     private OAuthValue(AccessToken temporaryAccess, Map<String, List<String>> requestMetadata) {
516       this.temporaryAccess = temporaryAccess;
517       this.requestMetadata = requestMetadata;
518     }
519 
520     @Override
equals(Object obj)521     public boolean equals(Object obj) {
522       if (!(obj instanceof OAuthValue)) {
523         return false;
524       }
525       OAuthValue other = (OAuthValue) obj;
526       return Objects.equals(this.requestMetadata, other.requestMetadata)
527           && Objects.equals(this.temporaryAccess, other.temporaryAccess);
528     }
529 
530     @Override
hashCode()531     public int hashCode() {
532       return Objects.hash(temporaryAccess, requestMetadata);
533     }
534   }
535 
536   enum CacheState {
537     FRESH,
538     STALE,
539     EXPIRED;
540   }
541 
542   static class FutureCallbackToMetadataCallbackAdapter implements FutureCallback<OAuthValue> {
543     private final RequestMetadataCallback callback;
544 
FutureCallbackToMetadataCallbackAdapter(RequestMetadataCallback callback)545     public FutureCallbackToMetadataCallbackAdapter(RequestMetadataCallback callback) {
546       this.callback = callback;
547     }
548 
549     @Override
onSuccess(@ullable OAuthValue value)550     public void onSuccess(@Nullable OAuthValue value) {
551       callback.onSuccess(value.requestMetadata);
552     }
553 
554     @Override
onFailure(Throwable throwable)555     public void onFailure(Throwable throwable) {
556       // refreshAccessToken will be invoked in an executor, so if it fails unwrap the underlying
557       // error
558       if (throwable instanceof ExecutionException) {
559         throwable = throwable.getCause();
560       }
561       callback.onFailure(throwable);
562     }
563   }
564 
565   /**
566    * Result from {@link com.google.auth.oauth2.OAuth2Credentials#getOrCreateRefreshTask()}.
567    *
568    * <p>Contains the the refresh task and a flag indicating if the task is newly created. If the
569    * task is newly created, it is the caller's responsibility to execute it.
570    */
571   static class AsyncRefreshResult {
572     private final RefreshTask task;
573     private final boolean isNew;
574 
AsyncRefreshResult(RefreshTask task, boolean isNew)575     AsyncRefreshResult(RefreshTask task, boolean isNew) {
576       this.task = task;
577       this.isNew = isNew;
578     }
579 
executeIfNew(Executor executor)580     void executeIfNew(Executor executor) {
581       if (isNew) {
582         executor.execute(task);
583       }
584     }
585   }
586 
587   @VisibleForTesting
588   class RefreshTaskListener implements Runnable {
589     private ListenableFutureTask<OAuthValue> task;
590 
RefreshTaskListener(ListenableFutureTask<OAuthValue> task)591     RefreshTaskListener(ListenableFutureTask<OAuthValue> task) {
592       this.task = task;
593     }
594 
595     @Override
run()596     public void run() {
597       finishRefreshAsync(task);
598     }
599   }
600 
601   class RefreshTask extends AbstractFuture<OAuthValue> implements Runnable {
602     private final ListenableFutureTask<OAuthValue> task;
603     private final RefreshTaskListener listener;
604 
RefreshTask(ListenableFutureTask<OAuthValue> task, RefreshTaskListener listener)605     RefreshTask(ListenableFutureTask<OAuthValue> task, RefreshTaskListener listener) {
606       this.task = task;
607       this.listener = listener;
608 
609       // Update Credential state first
610       task.addListener(listener, MoreExecutors.directExecutor());
611 
612       // Then notify the world
613       Futures.addCallback(
614           task,
615           new FutureCallback<OAuthValue>() {
616             @Override
617             public void onSuccess(OAuthValue result) {
618               RefreshTask.this.set(result);
619             }
620 
621             @Override
622             public void onFailure(Throwable t) {
623               RefreshTask.this.setException(t);
624             }
625           },
626           MoreExecutors.directExecutor());
627     }
628 
getTask()629     public ListenableFutureTask<OAuthValue> getTask() {
630       return this.task;
631     }
632 
633     @Override
run()634     public void run() {
635       task.run();
636     }
637   }
638 
639   public static class Builder {
640 
641     private AccessToken accessToken;
642     private Duration refreshMargin = DEFAULT_REFRESH_MARGIN;
643     private Duration expirationMargin = DEFAULT_EXPIRATION_MARGIN;
644 
Builder()645     protected Builder() {}
646 
Builder(OAuth2Credentials credentials)647     protected Builder(OAuth2Credentials credentials) {
648       this.accessToken = credentials.getAccessToken();
649       this.refreshMargin = credentials.refreshMargin;
650       this.expirationMargin = credentials.expirationMargin;
651     }
652 
653     @CanIgnoreReturnValue
setAccessToken(AccessToken token)654     public Builder setAccessToken(AccessToken token) {
655       this.accessToken = token;
656       return this;
657     }
658 
659     @CanIgnoreReturnValue
setRefreshMargin(Duration refreshMargin)660     public Builder setRefreshMargin(Duration refreshMargin) {
661       this.refreshMargin = refreshMargin;
662       return this;
663     }
664 
getRefreshMargin()665     public Duration getRefreshMargin() {
666       return refreshMargin;
667     }
668 
669     @CanIgnoreReturnValue
setExpirationMargin(Duration expirationMargin)670     public Builder setExpirationMargin(Duration expirationMargin) {
671       this.expirationMargin = expirationMargin;
672       return this;
673     }
674 
getExpirationMargin()675     public Duration getExpirationMargin() {
676       return expirationMargin;
677     }
678 
getAccessToken()679     public AccessToken getAccessToken() {
680       return accessToken;
681     }
682 
build()683     public OAuth2Credentials build() {
684       return new OAuth2Credentials(accessToken, refreshMargin, expirationMargin);
685     }
686   }
687 }
688