Skip to content

Commit 1808caa

Browse files
authored
Merge pull request #48296 from michalvavrik/feature/oidc-client-refresh-timer
OIDC Client: Add periodic asynchronous tokens refresh for performance critical applications
2 parents a0659db + 5f87344 commit 1808caa

File tree

15 files changed

+384
-95
lines changed

15 files changed

+384
-95
lines changed

docs/src/main/asciidoc/security-openid-connect-client-reference.adoc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,16 @@ You can also inject named `Tokens`, see <<named-oidc-clients,Inject named OidcCl
713713
`OidcClientRequestReactiveFilter`, `OidcClientRequestFilter` and `Tokens` producers will refresh the current expired access token if the refresh token is available.
714714
Additionally, the `quarkus.oidc-client.refresh-token-time-skew` property can be used for a preemptive access token refreshment to avoid sending nearly expired access tokens that might cause HTTP 401 errors. For example, if this property is set to `3S` and the access token will expire in less than 3 seconds, then this token will be auto-refreshed.
715715

716+
717+
By default, OIDC client refreshes the token during the current request, when it detects that it has expired, or nearly expired if the [refresh token time skew](https://quarkus.io/guides/security-openid-connect-client-reference#quarkus-oidc-client_quarkus-oidc-client-refresh-token-time-skew) is configured.
718+
Performance critical applications may want to avoid having to wait for a possible token refresh during the incoming requests and configure an asynchronous token refresh instead, for example:
719+
720+
[source,properties]
721+
----
722+
quarkus.oidc-client.refresh-interval=1m <1>
723+
----
724+
<1> Check every minute if the current access token is expired and must be refreshed.
725+
716726
If the access token needs to be refreshed, but no refresh token is available, then an attempt is made to acquire a new token by using a configured grant, such as `client_credentials`.
717727

718728
Some OpenID Connect Providers will not return a refresh token in a `client_credentials` grant response. For example, starting from Keycloak 12, a refresh token will not be returned by default for `client_credentials`. The providers might also restrict the number of times a refresh token can be used.

extensions/oidc-client/deployment/src/main/java/io/quarkus/oidc/client/deployment/OidcClientBuildStep.java

Lines changed: 12 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import jakarta.enterprise.context.RequestScoped;
1313
import jakarta.inject.Singleton;
1414

15+
import org.jboss.jandex.ClassType;
1516
import org.jboss.jandex.DotName;
1617

1718
import io.quarkus.arc.BeanDestroyer;
@@ -46,12 +47,10 @@
4647
import io.quarkus.oidc.client.runtime.OidcClientBuildTimeConfig;
4748
import io.quarkus.oidc.client.runtime.OidcClientDefaultIdConfigBuilder;
4849
import io.quarkus.oidc.client.runtime.OidcClientRecorder;
49-
import io.quarkus.oidc.client.runtime.OidcClientsConfig;
50+
import io.quarkus.oidc.client.runtime.OidcClientsImpl;
5051
import io.quarkus.oidc.client.runtime.TokenProviderProducer;
5152
import io.quarkus.oidc.client.runtime.TokensHelper;
5253
import io.quarkus.oidc.client.runtime.TokensProducer;
53-
import io.quarkus.tls.deployment.spi.TlsRegistryBuildItem;
54-
import io.quarkus.vertx.core.deployment.CoreVertxBuildItem;
5554

5655
@BuildSteps(onlyIf = OidcClientBuildStep.IsEnabled.class)
5756
public class OidcClientBuildStep {
@@ -98,46 +97,25 @@ void initOidcClients(OidcClientRecorder recorder) {
9897
recorder.initOidcClients();
9998
}
10099

101-
@Record(ExecutionTime.RUNTIME_INIT)
102100
@BuildStep
103-
public void setup(
104-
OidcClientsConfig oidcConfig,
105-
OidcClientRecorder recorder,
106-
CoreVertxBuildItem vertxBuildItem,
107-
OidcClientNamesBuildItem oidcClientNames,
108-
TlsRegistryBuildItem tlsRegistry,
109-
BuildProducer<SyntheticBeanBuildItem> syntheticBean) {
110-
111-
syntheticBean.produce(SyntheticBeanBuildItem.configure(OidcClients.class).unremovable()
112-
.types(OidcClients.class)
113-
.supplier(recorder.createOidcClientsBean(oidcConfig, vertxBuildItem.getVertx(), tlsRegistry.registry()))
114-
.scope(Singleton.class)
115-
.setRuntimeInit()
116-
.destroyer(BeanDestroyer.CloseableDestroyer.class)
117-
.done());
118-
119-
syntheticBean.produce(SyntheticBeanBuildItem.configure(OidcClient.class).unremovable()
120-
.types(OidcClient.class)
121-
.supplier(recorder.createOidcClientBean())
122-
.scope(Singleton.class)
123-
.setRuntimeInit()
124-
.destroyer(BeanDestroyer.CloseableDestroyer.class)
125-
.done());
126-
127-
produceNamedOidcClientBeans(syntheticBean, oidcClientNames.oidcClientNames(), recorder);
101+
AdditionalBeanBuildItem createOidcClientsBean() {
102+
return AdditionalBeanBuildItem.unremovableOf(OidcClientsImpl.class);
128103
}
129104

130-
private void produceNamedOidcClientBeans(BuildProducer<SyntheticBeanBuildItem> syntheticBean,
131-
Set<String> injectedOidcClientNames, OidcClientRecorder recorder) {
132-
injectedOidcClientNames.stream()
105+
@Record(ExecutionTime.RUNTIME_INIT)
106+
@BuildStep
107+
void produceNamedOidcClientBeans(OidcClientRecorder recorder, OidcClientNamesBuildItem oidcClientNames,
108+
BuildProducer<SyntheticBeanBuildItem> syntheticBean) {
109+
oidcClientNames.oidcClientNames().stream()
133110
.map(clientName -> syntheticNamedOidcClientBeanFor(clientName, recorder))
134111
.forEach(syntheticBean::produce);
135112
}
136113

137-
private SyntheticBeanBuildItem syntheticNamedOidcClientBeanFor(String clientName, OidcClientRecorder recorder) {
114+
private static SyntheticBeanBuildItem syntheticNamedOidcClientBeanFor(String clientName, OidcClientRecorder recorder) {
138115
return SyntheticBeanBuildItem.configure(OidcClient.class).unremovable()
139116
.types(OidcClient.class)
140-
.supplier(recorder.createOidcClientBean(clientName))
117+
.addInjectionPoint(ClassType.create(OidcClients.class))
118+
.createWith(recorder.createOidcClientBean(clientName))
141119
.scope(Singleton.class)
142120
.addQualifier().annotation(NamedOidcClient.class).addValue("value", clientName).done()
143121
.setRuntimeInit()

extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfig.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
public class OidcClientConfig extends OidcClientCommonConfig implements io.quarkus.oidc.client.runtime.OidcClientConfig {
1717

1818
public OidcClientConfig() {
19-
19+
this.refreshInterval = Optional.empty();
2020
}
2121

2222
public OidcClientConfig(io.quarkus.oidc.client.runtime.OidcClientConfig mapping) {
@@ -32,6 +32,7 @@ public OidcClientConfig(io.quarkus.oidc.client.runtime.OidcClientConfig mapping)
3232
grantOptions = mapping.grantOptions();
3333
earlyTokensAcquisition = mapping.earlyTokensAcquisition();
3434
headers = mapping.headers();
35+
refreshInterval = mapping.refreshInterval();
3536
}
3637

3738
/**
@@ -78,6 +79,8 @@ public OidcClientConfig(io.quarkus.oidc.client.runtime.OidcClientConfig mapping)
7879

7980
public Grant grant = new Grant();
8081

82+
private final Optional<Duration> refreshInterval;
83+
8184
@Override
8285
public Optional<String> id() {
8386
return id;
@@ -133,6 +136,11 @@ public Map<String, String> headers() {
133136
return headers;
134137
}
135138

139+
@Override
140+
public Optional<Duration> refreshInterval() {
141+
return refreshInterval;
142+
}
143+
136144
public static class Grant implements io.quarkus.oidc.client.runtime.OidcClientConfig.Grant {
137145

138146
@Override

extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfigBuilder.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ private static class OidcClientConfigImpl extends OidcClientCommonConfigImpl imp
3333
private final Optional<List<String>> scopes;
3434
private final boolean clientEnabled;
3535
private final Optional<String> id;
36+
private final Optional<Duration> refreshInterval;;
3637

3738
private OidcClientConfigImpl(OidcClientConfigBuilder builder) {
3839
super(builder);
@@ -47,6 +48,7 @@ private OidcClientConfigImpl(OidcClientConfigBuilder builder) {
4748
this.scopes = builder.scopes.isEmpty() ? Optional.empty() : Optional.of(List.copyOf(builder.scopes));
4849
this.clientEnabled = builder.clientEnabled;
4950
this.id = builder.id;
51+
this.refreshInterval = builder.refreshInterval;
5052
}
5153

5254
@Override
@@ -103,6 +105,11 @@ public boolean earlyTokensAcquisition() {
103105
public Map<String, String> headers() {
104106
return headers;
105107
}
108+
109+
@Override
110+
public Optional<Duration> refreshInterval() {
111+
return refreshInterval;
112+
}
106113
}
107114

108115
/**
@@ -123,6 +130,7 @@ public Map<String, String> headers() {
123130
private Optional<Duration> refreshTokenTimeSkew;
124131
private boolean clientEnabled;
125132
private Optional<String> id;
133+
private Optional<Duration> refreshInterval;
126134

127135
/**
128136
* Creates {@link OidcClientConfigBuilder} builder populated with documented default values.
@@ -146,6 +154,7 @@ public OidcClientConfigBuilder(OidcClientConfig config) {
146154
this.refreshTokenTimeSkew = config.refreshTokenTimeSkew();
147155
this.clientEnabled = config.clientEnabled();
148156
this.id = config.id();
157+
this.refreshInterval = config.refreshInterval();
149158
if (config.scopes().isPresent()) {
150159
this.scopes.addAll(config.scopes().get());
151160
}
@@ -330,6 +339,11 @@ public GrantBuilder grant() {
330339
return new GrantBuilder(this);
331340
}
332341

342+
public OidcClientConfigBuilder refreshInterval(Duration refreshInterval) {
343+
this.refreshInterval = Optional.ofNullable(refreshInterval);
344+
return this;
345+
}
346+
333347
/**
334348
* @return OidcClientConfig
335349
*/

extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/AbstractTokensProducer.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@
33
import java.util.Map;
44
import java.util.Objects;
55
import java.util.Optional;
6+
import java.util.function.Supplier;
67

78
import jakarta.annotation.PostConstruct;
9+
import jakarta.enterprise.inject.Instance;
810
import jakarta.inject.Inject;
911

1012
import org.jboss.logging.Logger;
1113

12-
import io.quarkus.arc.Arc;
1314
import io.quarkus.oidc.client.OidcClient;
14-
import io.quarkus.oidc.client.OidcClients;
1515
import io.quarkus.oidc.client.Tokens;
1616
import io.smallrye.mutiny.Uni;
1717

@@ -26,6 +26,8 @@ public abstract class AbstractTokensProducer {
2626
public OidcClientsConfig oidcClientsConfig;
2727
@Inject
2828
public OidcClientBuildTimeConfig oidcClientBuildTimeConfig;
29+
@Inject
30+
public Instance<OidcClientsImpl> oidcClientsInstance;
2931

3032
final TokensHelper tokensHelper = new TokensHelper();
3133

@@ -36,10 +38,10 @@ public void init() {
3638
+ " skipping the token producer initialization");
3739
return;
3840
}
41+
final OidcClientsImpl oidcClients = oidcClientsInstance.get();
3942
Optional<OidcClient> initializedClient = client();
4043
if (initializedClient.isEmpty()) {
4144
Optional<String> clientId = Objects.requireNonNull(clientId(), "clientId must not be null");
42-
OidcClients oidcClients = Arc.container().instance(OidcClients.class).get();
4345
if (clientId.isPresent()) {
4446
// static named OidcClient
4547
oidcClient = Objects.requireNonNull(oidcClients.getClient(clientId.get()), "Unknown client");
@@ -54,6 +56,14 @@ public void init() {
5456
}
5557

5658
initTokens();
59+
if (!isForceNewTokens()) {
60+
oidcClients.registerTokenRefresh(oidcClient, new Supplier<Uni<Tokens>>() {
61+
@Override
62+
public Uni<Tokens> get() {
63+
return getTokens();
64+
}
65+
});
66+
}
5767
}
5868

5969
protected boolean isClientFeatureDisabled() {

extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientConfig.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import java.util.Map;
66
import java.util.Optional;
77

8+
import io.quarkus.oidc.client.OidcClient;
89
import io.quarkus.oidc.client.OidcClientConfigBuilder;
910
import io.quarkus.oidc.common.runtime.OidcConstants;
1011
import io.quarkus.oidc.common.runtime.config.OidcClientCommonConfig;
@@ -172,6 +173,16 @@ public String getGrantType() {
172173
*/
173174
Map<String, String> headers();
174175

176+
/**
177+
* Token refresh interval.
178+
* By default, OIDC client refreshes the token during the current request, when it detects that it has expired,
179+
* or nearly expired if the {@link #refreshTokenTimeSkew()} is configured.
180+
* But, when this property is configured, OIDC client can refresh the token asynchronously in the configured interval.
181+
* This property is only effective with OIDC client filters and other {@link AbstractTokensProducer} extensions,
182+
* but not when you use the {@link OidcClient#getTokens()} API directly.
183+
*/
184+
Optional<Duration> refreshInterval();
185+
175186
/**
176187
* Creates {@link OidcClientConfigBuilder} builder populated with documented default values.
177188
*

extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientImpl.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,4 +366,8 @@ private HttpRequest<Buffer> filterHttpRequest(
366366
}
367367
return request;
368368
}
369+
370+
OidcClientConfig getConfig() {
371+
return oidcConfig;
372+
}
369373
}

extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientRecorder.java

Lines changed: 12 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77
import java.util.Set;
88
import java.util.function.BiFunction;
99
import java.util.function.Function;
10-
import java.util.function.Supplier;
1110
import java.util.stream.Collectors;
1211

1312
import jakarta.enterprise.inject.CreationException;
1413

1514
import org.jboss.logging.Logger;
1615

1716
import io.quarkus.arc.Arc;
17+
import io.quarkus.arc.SyntheticCreationalContext;
1818
import io.quarkus.oidc.client.OidcClient;
1919
import io.quarkus.oidc.client.OidcClientException;
2020
import io.quarkus.oidc.client.OidcClients;
@@ -29,7 +29,6 @@
2929
import io.quarkus.oidc.common.runtime.OidcTlsSupport;
3030
import io.quarkus.runtime.annotations.Recorder;
3131
import io.quarkus.runtime.configuration.ConfigurationException;
32-
import io.quarkus.tls.TlsConfigurationRegistry;
3332
import io.smallrye.mutiny.Uni;
3433
import io.vertx.core.Vertx;
3534
import io.vertx.ext.web.client.WebClientOptions;
@@ -43,13 +42,10 @@ public class OidcClientRecorder {
4342
private static final String CLIENT_ID_ATTRIBUTE = "client-id";
4443
static final String DEFAULT_OIDC_CLIENT_ID = "Default";
4544

46-
private static OidcClients setup(OidcClientsConfig oidcClientsConfig, Supplier<Vertx> vertx,
47-
Supplier<TlsConfigurationRegistry> registrySupplier) {
45+
static Map<String, OidcClient> createStaticOidcClients(OidcClientsConfig oidcClientsConfig, Vertx vertx,
46+
OidcTlsSupport tlsSupport, OidcClientConfig defaultClientConfig) {
4847

49-
var tlsSupport = OidcTlsSupport.of(registrySupplier);
50-
var defaultClientConfig = OidcClientsConfig.getDefaultClient(oidcClientsConfig);
5148
String defaultClientId = defaultClientConfig.id().get();
52-
OidcClient defaultClient = createOidcClient(defaultClientConfig, defaultClientId, vertx, tlsSupport);
5349

5450
Map<String, OidcClient> staticOidcClients = new HashMap<>();
5551

@@ -62,54 +58,26 @@ private static OidcClients setup(OidcClientsConfig oidcClientsConfig, Supplier<V
6258
}
6359
}
6460

65-
return new OidcClientsImpl(defaultClient, staticOidcClients,
66-
new Function<OidcClientConfig, Uni<OidcClient>>() {
67-
@Override
68-
public Uni<OidcClient> apply(OidcClientConfig config) {
69-
return createOidcClientUni(config, config.id().get(), vertx, OidcTlsSupport.of(registrySupplier));
70-
}
71-
});
72-
}
73-
74-
public Supplier<OidcClient> createOidcClientBean() {
75-
return new Supplier<OidcClient>() {
76-
77-
@Override
78-
public OidcClient get() {
79-
return Arc.container().instance(OidcClients.class).get().getClient();
80-
}
81-
};
82-
}
83-
84-
public Supplier<OidcClient> createOidcClientBean(String clientName) {
85-
return new Supplier<OidcClient>() {
86-
87-
@Override
88-
public OidcClient get() {
89-
return Arc.container().instance(OidcClients.class).get().getClient(clientName);
90-
}
91-
};
61+
return Map.copyOf(staticOidcClients);
9262
}
9363

94-
public Supplier<OidcClients> createOidcClientsBean(OidcClientsConfig oidcClientsConfig, Supplier<Vertx> vertx,
95-
Supplier<TlsConfigurationRegistry> registrySupplier) {
96-
return new Supplier<OidcClients>() {
97-
64+
public Function<SyntheticCreationalContext<OidcClient>, OidcClient> createOidcClientBean(String clientName) {
65+
return new Function<SyntheticCreationalContext<OidcClient>, OidcClient>() {
9866
@Override
99-
public OidcClients get() {
100-
return setup(oidcClientsConfig, vertx, registrySupplier);
67+
public OidcClient apply(SyntheticCreationalContext<OidcClient> ctx) {
68+
return ctx.getInjectedReference(OidcClients.class).getClient(clientName);
10169
}
10270
};
10371
}
10472

105-
protected static OidcClient createOidcClient(OidcClientConfig oidcConfig, String oidcClientId, Supplier<Vertx> vertx,
73+
protected static OidcClient createOidcClient(OidcClientConfig oidcConfig, String oidcClientId, Vertx vertx,
10674
OidcTlsSupport tlsSupport) {
10775
return createOidcClientUni(oidcConfig, oidcClientId, vertx, tlsSupport).await()
10876
.atMost(oidcConfig.connectionTimeout());
10977
}
11078

11179
protected static Uni<OidcClient> createOidcClientUni(OidcClientConfig oidcConfig, String oidcClientId,
112-
Supplier<Vertx> vertx, OidcTlsSupport tlsSupport) {
80+
Vertx vertx, OidcTlsSupport tlsSupport) {
11381
if (!oidcConfig.clientEnabled()) {
11482
String message = String.format("'%s' client configuration is disabled", oidcClientId);
11583
LOG.debug(message);
@@ -139,7 +107,7 @@ protected static Uni<OidcClient> createOidcClientUni(OidcClientConfig oidcConfig
139107
options.setFollowRedirects(oidcConfig.followRedirects());
140108
OidcCommonUtils.setHttpClientOptions(oidcConfig, options, tlsSupport.forConfig(oidcConfig.tls()));
141109

142-
var mutinyVertx = new io.vertx.mutiny.core.Vertx(vertx.get());
110+
var mutinyVertx = new io.vertx.mutiny.core.Vertx(vertx);
143111
WebClient client = WebClient.create(mutinyVertx, options);
144112

145113
Map<OidcEndpoint.Type, List<OidcRequestFilter>> oidcRequestFilters = OidcCommonUtils.getOidcRequestFilters();
@@ -219,7 +187,7 @@ public OidcClient apply(OidcConfigurationMetadata metadata, Throwable t) {
219187

220188
return new OidcClientImpl(client, metadata.tokenRequestUri, metadata.tokenRevokeUri, grantType,
221189
tokenGrantParams, commonRefreshGrantParams, oidcConfig, oidcRequestFilters,
222-
oidcResponseFilters, vertx.get());
190+
oidcResponseFilters, vertx);
223191
}
224192

225193
});

0 commit comments

Comments
 (0)