From 5d85b0ae90edbe9047e391056cccb62a73994fba Mon Sep 17 00:00:00 2001 From: David Kral Date: Thu, 1 Sep 2022 17:41:18 +0200 Subject: [PATCH] Access token refresh Signed-off-by: David Kral --- .../java/io/helidon/security/jwt/Jwt.java | 12 +++++--- .../mapper/IdcsMtRoleMapperRxProvider.java | 3 +- .../idcs/mapper/IdcsRoleMapperRxProvider.java | 2 +- .../mapper/IdcsRoleMapperRxProviderBase.java | 18 ++++++++++-- .../providers/oidc/common/OidcConfig.java | 28 +++++++++++++++++++ 5 files changed, 54 insertions(+), 9 deletions(-) diff --git a/security/jwt/src/main/java/io/helidon/security/jwt/Jwt.java b/security/jwt/src/main/java/io/helidon/security/jwt/Jwt.java index 316baff9cc3..8ef8950f250 100644 --- a/security/jwt/src/main/java/io/helidon/security/jwt/Jwt.java +++ b/security/jwt/src/main/java/io/helidon/security/jwt/Jwt.java @@ -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; } @@ -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; } } diff --git a/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsMtRoleMapperRxProvider.java b/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsMtRoleMapperRxProvider.java index 8789c103822..c7402fe226a 100644 --- a/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsMtRoleMapperRxProvider.java +++ b/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsMtRoleMapperRxProvider.java @@ -292,7 +292,8 @@ protected Single> getGrantsFromServer(String idcsTenantId, protected Single> 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); } diff --git a/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperRxProvider.java b/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperRxProvider.java index 0eb5891c23e..c4d401e9773 100644 --- a/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperRxProvider.java +++ b/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperRxProvider.java @@ -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()); } /** diff --git a/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperRxProviderBase.java b/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperRxProviderBase.java index a4fd97ae2f2..d37dbc84d45 100644 --- a/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperRxProviderBase.java +++ b/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperRxProviderBase.java @@ -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; @@ -384,10 +386,12 @@ protected static class AppTokenRx { private final AtomicReference> 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> getToken(RoleMapTracing tracing) { @@ -406,8 +410,10 @@ protected Single> 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 future = new CompletableFuture<>(); if (token.compareAndSet(currentTokenData, future)) { fromServer(tracing, future); @@ -423,6 +429,12 @@ protected Single> 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 future) { Parameters params = Parameters.builder("idcs-form-params") .add("grant_type", "client_credentials") diff --git a/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcConfig.java b/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcConfig.java index 4df090a7215..5785775c29e 100644 --- a/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcConfig.java +++ b/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcConfig.java @@ -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()); @@ -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; @@ -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); @@ -973,6 +976,16 @@ 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. @@ -1109,6 +1122,7 @@ public static class Builder implements io.helidon.common.Builder