All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.oauth2.client.AutoRenewingTokenProvider Maven / Gradle / Ivy

/* 
 * Copyright (c) 2017 Georgi Pavlov ([email protected]).
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the MIT license which accompanies 
 * this distribution, and is available at 
 * https://github.com/tengia/oauth-2/blob/master/LICENSE
 */
package net.oauth2.client;

import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import commons.util.Observable;
import commons.util.ObservableMixin;
import net.oauth2.AccessToken;
import net.oauth2.TemporalAccessToken;

/**
 * A TokenProvider implementation that leverages the OAuth token infrastructure
 * to auto-renew tokens asynchronously and always be able to provide a valid
 * token to consumers as long as the Token Service can supply them.
 * 
 * 
    *
  • Asynchronous renewal of expiring Access Tokens
  • *
  • Push notifications for token changes to subscribers
  • *
  • Auto recovery with retry policies for resilient implementations
  • *
  • Auto adjust to Token Service capabilities to refresh or always fetch new * Access Token as appropriate
  • *
  • Resumable, can survive restarts*
  • *
  • Configurable renew schedule for efficient networking
  • *
*
* Persistence of tokens is not in scope for this client. An * external actor taking care of this is required to recover and resume renewal * of tokens.
* * @param * The type of the AccessToken managed by this TokenProvider. Can be * a subclass of AccessToken. */ public class AutoRenewingTokenProvider implements TokenProvider, Resumable, TokenChangeObservable, TokenProviderJob { private final static Logger LOGGER = LoggerFactory.getLogger(AutoRenewingTokenProvider.class); private final TokenService tokenService; ScheduledExecutorService schedulerExecutor; private RetryPolicy retryPolicy; private Observable> observable; TokenRenewTask tokenRenewTask; ScheduledFuture future; private TemporalUnit tokenExpireInTimeUnits = ChronoUnit.SECONDS;// used by // java.time.Instance private double delayModifier = 0.9; private boolean strictlyRefresh = false; public AutoRenewingTokenProvider(final TokenService tokenService, ScheduledExecutorService executor, RetryPolicy retryPolicy, Observable> observable, boolean strictlyRefresh) { this.tokenService = tokenService; this.schedulerExecutor = executor; this.retryPolicy = retryPolicy; this.observable = observable; this.strictlyRefresh = strictlyRefresh; } /** * Default constructor utilizing the provided TokenService. Configures this * instance with single thread scheduler service, no retry policy, * ObservableMixin observable and non strict refresh policy. * * @param tokenService * the service delegate used to fetch and refresh tokens by this * instance. */ public AutoRenewingTokenProvider(final TokenService tokenService) { this(tokenService, Executors.newSingleThreadScheduledExecutor(), new NoRetryPolicy(), new ObservableMixin>(), false); } class TokenRenewTask implements Runnable { private final AutoRenewingTokenProvider svc; TemporalAccessToken token; TokenRenewTask(AutoRenewingTokenProvider svc, TemporalAccessToken token) { this.svc = svc; this.token = token; if (token.token().getRefreshToken() == null && this.svc.strictlyRefresh()) throw new IllegalArgumentException( "Cannot start refresh token timer without a valid token with refresh_token value when set to strictly refresh"); } @Override public void run() { TemporalAccessToken newToken = null; int retries = 0; RetryPolicy retryPolicy = this.svc.getRetryPolicy(); while (newToken == null && retries > -1 && retries < retryPolicy.maxRetries()) { retries++; try { newToken = this.svc.renew(this.token); // Update Access Token provisioned by this provider only if // this is the last attempt. Intermediate nulls will not be // considered if (newToken != null || (newToken == null && retries == retryPolicy.maxRetries())) { TemporalAccessToken previousToken = this.token; this.token = newToken; this.svc.fireTokenUpdate(this.token, previousToken); } } catch (IOException e) { LOGGER.error("Token refresh task failed", e); if (retryPolicy.onException(e)) { try { TimeUnit.MILLISECONDS.sleep(retryPolicy.periodBetweenRetries()); } catch (InterruptedException ie) { } } else { break; } } } } TemporalAccessToken getToken() { return this.token; } } protected TemporalAccessToken renew(TemporalAccessToken token) throws OAuth2ProtocolException, IOException { T newToken = null; String refreshToken = token.token().getRefreshToken(); // automatically fallback to fetch new token if refreshToken is null, // unless instructed otherwise if (refreshToken == null && !strictlyRefresh()) newToken = this.getTokenService().fetch(); if (newToken == null) newToken = this.getTokenService().refresh(refreshToken); if (newToken == null) return null; TemporalAccessToken temporalToken = new TemporalAccessToken<>(newToken, Instant.now(), this.tokenExpireInTemporalUnit()); return temporalToken; } public AutoRenewingTokenProvider strictlyRefresh(boolean strictlyRefresh) { this.strictlyRefresh = strictlyRefresh; return this; } boolean strictlyRefresh() { return this.strictlyRefresh; } TokenService getTokenService() { return this.tokenService; } RetryPolicy getRetryPolicy() { return this.retryPolicy; } void fireTokenUpdate(final TemporalAccessToken token, final TemporalAccessToken previous) { // Notify the list of registered listeners if (this.observable != null) { try { this.observable.notify((listener) -> listener.tokenChanged(token, previous)); } catch (Throwable t) { LOGGER.error("Change listener error", t); } } } /** * Sets the time unit of measure (e.g. ChronoUnit.SECONDS, * ChronUnit.MINUTES, etc.) for the expires_in property of access * tokens. Designed for chaining. * * @param temporalUnit * @return owning instance for chaining. */ public AutoRenewingTokenProvider tokenExpireInTemporalUnit(TemporalUnit temporalUnit) { this.tokenExpireInTimeUnits = temporalUnit; return this; } TemporalUnit tokenExpireInTemporalUnit() { return this.tokenExpireInTimeUnits; } /* * (non-Javadoc) * * @see net.oauth2.client.ScheduledRefreshing#schedule(double) */ @Override public AutoRenewingTokenProvider schedule(double delayModifier) { if (!(delayModifier > 0) || delayModifier > 1) throw new IllegalArgumentException("delayModifier must be value between (0-1]"); this.delayModifier = delayModifier; return this; } double delayModifier() { return this.delayModifier; } @Override public Duration estimatedRepetitionsDelay() { T token = this.get(); if (token == null) throw new IllegalStateException("No token to estimate for"); if (token.getExpiresIn() < 1) throw new IllegalArgumentException("The token has no valid expires_in property: " + token.getExpiresIn()); TemporalUnit ttlUnit = this.tokenRenewTask.getToken().ttlUnit(); long delay = Math.round(token.getExpiresIn() * this.delayModifier); Duration delayDuraiton = Duration.of(delay, ttlUnit); return delayDuraiton; } /** * Converts a {@code TimeUnit} to a {@code ChronoUnit}. *

* This handles the seven units declared in {@code TimeUnit}. * * @param unit * the unit to convert, not null * @return the converted unit, not null */ static ChronoUnit chronoUnit(TimeUnit unit) { if (unit == null) throw new IllegalArgumentException(); switch (unit) { case NANOSECONDS: return ChronoUnit.NANOS; case MICROSECONDS: return ChronoUnit.MICROS; case MILLISECONDS: return ChronoUnit.MILLIS; case SECONDS: return ChronoUnit.SECONDS; case MINUTES: return ChronoUnit.MINUTES; case HOURS: return ChronoUnit.HOURS; case DAYS: return ChronoUnit.DAYS; default: throw new IllegalArgumentException("Unknown TimeUnit constant"); } } public AutoRenewingTokenProvider setRetryPolicy(RetryPolicy retryPolicy) { if (retryPolicy == null) throw new IllegalArgumentException("retryPolicy is null"); this.retryPolicy = retryPolicy; return this; } /* * (non-Javadoc) * * @see net.oauth2.client.ScheduledRefreshing#start() */ @Override public ScheduledFuture start() throws IOException { if (this.isActive()) throw new IllegalStateException("Already started"); // fetching from a remote service will inevitably pose some delay so we // defensively choose to count the fetch time from the very start. T newToken = this.getTokenService().fetch(); if (newToken == null) throw new IllegalStateException("The token fetched from this TokenService is null"); TemporalAccessToken accessToken = new TemporalAccessToken<>(newToken, Instant.now(), this.tokenExpireInTemporalUnit()); this.fireTokenUpdate(accessToken, null); this.tokenRenewTask = new TokenRenewTask(this, accessToken); long delayMillis = this.estimatedRepetitionsDelay().toMillis(); this.future = this.schedulerExecutor.scheduleWithFixedDelay(this.tokenRenewTask, 0L, delayMillis, TimeUnit.MILLISECONDS); return this.future; } public boolean isActive() { return this.future != null && !this.future.isDone(); } /* * (non-Javadoc) * * @see net.oauth2.client.ScheduledRefreshing#stop(boolean) */ @Override public void stop(boolean graceful) { // silently ignore if executor not started if (this.future != null) { if (graceful) this.schedulerExecutor.shutdown(); else this.schedulerExecutor.shutdownNow(); this.future = null; } } /* * (non-Javadoc) * * @see net.oauth2.client.Resumble#resume(T, java.time.Instant, boolean) */ @Override public ScheduledFuture resume(T token, Instant fetchMoment, boolean refetchIfExpired) { if (this.isActive()) throw new IllegalStateException("Already started"); if (token == null) throw new IllegalArgumentException("Cannot resume with token null"); // fetching from a remote service will inevitably pose some delay so we // defensively choose to count the fetch time from the very start. TemporalAccessToken _token = new TemporalAccessToken<>(token, fetchMoment); if (_token.isExpired() && !refetchIfExpired) throw new IllegalStateException("Cannot resume an expired token"); this.fireTokenUpdate(_token, null); this.tokenRenewTask = new TokenRenewTask(this, _token); long delayMillis = this.estimatedRepetitionsDelay().toMillis(); this.future = this.schedulerExecutor.scheduleWithFixedDelay(this.tokenRenewTask, _token.ttlLeft(ChronoUnit.MILLIS), delayMillis, TimeUnit.MILLISECONDS); return this.future; } public void suspend(final boolean graceful) { this.stop(graceful); } /** * Provides a (cached) Access Token upon invocation. The Access Token is * transparently and asynchronously refreshed upon expiration. Consider * using the observe() method for push notification on changes. */ /* * (non-Javadoc) * * @see net.oauth2.client.TokenProvider#get() */ @SuppressWarnings("unchecked") @Override public T get() { if (this.tokenRenewTask == null || this.tokenRenewTask.getToken() == null) return null; return this.tokenRenewTask.getToken().token(); } /* * (non-Javadoc) * * @see net.oauth2.client.TokenChangePushing#observe(net.oauth2.client. * TokenChangeListener) */ @Override public AutoRenewingTokenProvider attach(TokenChangeObserver changeObserver) { if (this.observable == null) throw new UnsupportedOperationException("This instance is not configured with observable"); this.observable.attach(changeObserver); return this; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy