Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Access token refresh - 3.x #4821

Merged
merged 1 commit into from
Sep 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions security/jwt/src/main/java/io/helidon/security/jwt/Jwt.java
Original file line number Diff line number Diff line change
Expand Up @@ -986,14 +986,14 @@ private abstract static class InstantValidator extends OptionalValidator {
private final TemporalUnit allowedTimeSkewUnit;

private InstantValidator() {
this.instant = Instant.now();
this.instant = null;
this.allowedTimeSkewAmount = 5;
this.allowedTimeSkewUnit = ChronoUnit.SECONDS;
}

private InstantValidator(boolean mandatory) {
super(mandatory);
this.instant = Instant.now();
this.instant = null;
this.allowedTimeSkewAmount = 5;
this.allowedTimeSkewUnit = ChronoUnit.SECONDS;
}
Expand All @@ -1007,11 +1007,15 @@ private InstantValidator(Instant instant, int allowedTimeSkew, TemporalUnit allo
}

Instant latest() {
return instant.plus(allowedTimeSkewAmount, allowedTimeSkewUnit);
return instant().plus(allowedTimeSkewAmount, allowedTimeSkewUnit);
}

Instant earliest() {
return instant.minus(allowedTimeSkewAmount, allowedTimeSkewUnit);
return instant().minus(allowedTimeSkewAmount, allowedTimeSkewUnit);
}

Instant instant() {
return instant == null ? Instant.now() : instant;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,8 @@ protected Single<List<? extends Grant>> getGrantsFromServer(String idcsTenantId,
protected Single<Optional<String>> getAppToken(String idcsTenantId, RoleMapTracing tracing) {
// if cached and valid, use the cached token
return tokenCache.computeIfAbsent(idcsTenantId, key -> new AppTokenRx(oidcConfig().appWebClient(),
multitenantEndpoints.tokenEndpoint(idcsTenantId)))
multitenantEndpoints.tokenEndpoint(idcsTenantId),
oidcConfig().tokenRefreshSkew()))
.getToken(tracing);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ protected IdcsRoleMapperRxProvider(Builder<?> builder) {
this.asserterUri = oidcConfig.identityUri() + "/admin/v1/Asserter";
this.tokenEndpointUri = oidcConfig.tokenEndpointUri();

this.appToken = new AppTokenRx(oidcConfig.appWebClient(), tokenEndpointUri);
this.appToken = new AppTokenRx(oidcConfig.appWebClient(), tokenEndpointUri, oidcConfig.tokenRefreshSkew());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
package io.helidon.security.providers.idcs.mapper;

import java.net.URI;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.LinkedList;
Expand Down Expand Up @@ -383,10 +385,12 @@ protected static class AppTokenRx {
private final AtomicReference<CompletableFuture<AppTokenData>> token = new AtomicReference<>();
private final WebClient webClient;
private final URI tokenEndpointUri;
private final Duration tokenRefreshSkew;

protected AppTokenRx(WebClient webClient, URI tokenEndpointUri) {
protected AppTokenRx(WebClient webClient, URI tokenEndpointUri, Duration tokenRefreshSkew) {
this.webClient = webClient;
this.tokenEndpointUri = tokenEndpointUri;
this.tokenRefreshSkew = tokenRefreshSkew;
}

protected Single<Optional<String>> getToken(RoleMapTracing tracing) {
Expand All @@ -405,8 +409,10 @@ protected Single<Optional<String>> getToken(RoleMapTracing tracing) {
return Single.create(currentTokenData)
.flatMapSingle(tokenData -> {
Jwt jwt = tokenData.appJwt();
if (jwt == null || !tokenData.appJwt().validate(TIME_VALIDATORS).isValid()) {
// it is not valid - we must get a new value
if (jwt == null
|| !jwt.validate(TIME_VALIDATORS).isValid()
|| isNearExpiration(jwt)) {
// it is not valid or is very close to expiration - we must get a new value
CompletableFuture<AppTokenData> future = new CompletableFuture<>();
if (token.compareAndSet(currentTokenData, future)) {
fromServer(tracing, future);
Expand All @@ -422,6 +428,12 @@ protected Single<Optional<String>> getToken(RoleMapTracing tracing) {
});
}

private boolean isNearExpiration(Jwt jwt) {
return jwt.expirationTime()
.map(exp -> exp.minus(tokenRefreshSkew).isBefore(Instant.now()))
.orElse(false);
}

private void fromServer(RoleMapTracing tracing, CompletableFuture<AppTokenData> future) {
FormParams params = FormParams.builder()
.add("grant_type", "client_credentials")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ public final class OidcConfig {
static final int DEFAULT_MAX_REDIRECTS = 5;
static final int DEFAULT_TIMEOUT_SECONDS = 30;
static final boolean DEFAULT_FORCE_HTTPS_REDIRECTS = false;
static final Duration DEFAULT_TOKEN_REFRESH_SKEW = Duration.ofSeconds(5);

private static final Logger LOGGER = Logger.getLogger(OidcConfig.class.getName());
private static final JsonReaderFactory JSON = Json.createReaderFactory(Collections.emptyMap());
Expand Down Expand Up @@ -382,6 +383,7 @@ public final class OidcConfig {
private final URI logoutEndpointUri;
private final CrossOriginConfig crossOriginConfig;
private final boolean forceHttpsRedirects;
private final Duration tokenRefreshSkew;

private OidcConfig(Builder builder) {
this.clientId = builder.clientId;
Expand Down Expand Up @@ -454,6 +456,7 @@ private OidcConfig(Builder builder) {
}
}
this.crossOriginConfig = builder.crossOriginConfig;
this.tokenRefreshSkew = builder.tokenRefreshSkew;

LOGGER.finest(() -> "OIDC Scope audience: " + scopeAudience);
LOGGER.finest(() -> "Redirect URI with host: " + frontendUri + redirectUri);
Expand Down Expand Up @@ -973,6 +976,15 @@ public CrossOriginConfig crossOriginConfig() {
return crossOriginConfig;
}

/**
* Amount of time access token should be refreshed before its expiration time.
*
* @return refresh time skew
*/
public Duration tokenRefreshSkew() {
return tokenRefreshSkew;
}

/**
* Client Authentication methods that are used by Clients to authenticate to the Authorization
* Server when using the Token Endpoint.
Expand Down Expand Up @@ -1109,6 +1121,7 @@ public static class Builder implements io.helidon.common.Builder<Builder, OidcCo
private URI postLogoutUri;
private CrossOriginConfig crossOriginConfig;
private boolean forceHttpsRedirects = DEFAULT_FORCE_HTTPS_REDIRECTS;
private Duration tokenRefreshSkew = DEFAULT_TOKEN_REFRESH_SKEW;

@Override
public OidcConfig build() {
Expand Down Expand Up @@ -1320,6 +1333,20 @@ public Builder config(Config config) {

config.get("cors").as(CrossOriginConfig::create).ifPresent(this::crossOriginConfig);

config.get("token-refresh-before-expiration").as(Duration.class).ifPresent(this::tokenRefreshSkew);

return this;
}

/**
* Amount of time access token should be refreshed before its expiration time.
* Default is 5 seconds.
*
* @param tokenRefreshSkew time to refresh token before expiration
* @return updated builder
*/
public Builder tokenRefreshSkew(Duration tokenRefreshSkew) {
this.tokenRefreshSkew = tokenRefreshSkew;
return this;
}

Expand Down