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