Skip to content

Commit a044aab

Browse files
authored
Merge pull request #50629 from michalvavrik/feature/oidc-async-cred-provider
OIDC extensions: retrieve credentials from credentials provider asynchronously
2 parents 7d3b3eb + 19ae13e commit a044aab

File tree

13 files changed

+554
-123
lines changed

13 files changed

+554
-123
lines changed

extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientCredentialsTestCase.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ public void testGetTokensDefaultClient() {
3636
doTestGetTokensClient("default");
3737
}
3838

39+
@Test
40+
public void testClientSecretBasicAuthSchemeRefresh() {
41+
doTestGetTokenClient("named-1");
42+
}
43+
44+
@Test
45+
public void testClientSecretPostMethodRefresh() {
46+
doTestGetTokenClient("named-2");
47+
}
48+
3949
@Test
4050
public void testGetTokensOnDemand() {
4151
String[] tokens = RestAssured.when().get("/clients/tokenOnDemand").body().asString().split(" ");

extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/SecretProvider.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.util.HashMap;
44
import java.util.Map;
5+
import java.util.concurrent.atomic.AtomicInteger;
56

67
import jakarta.enterprise.context.ApplicationScoped;
78
import jakarta.inject.Named;
@@ -12,10 +13,23 @@
1213
@Named("vault-secret-provider")
1314
public class SecretProvider implements CredentialsProvider {
1415

16+
private final AtomicInteger namedClient1Counter = new AtomicInteger();
17+
private final AtomicInteger namedClient2Counter = new AtomicInteger();
18+
1519
@Override
1620
public Map<String, String> getCredentials(String credentialsProviderName) {
1721
Map<String, String> creds = new HashMap<>();
18-
creds.put("secret-from-vault", "secret");
22+
23+
if ("named-1-client-secret".equals(credentialsProviderName)) {
24+
boolean setCorrect = namedClient1Counter.incrementAndGet() == 2;
25+
creds.put("secret-from-vault", setCorrect ? "secret" : "wrong-secret");
26+
} else if ("named-2-client-secret".equals(credentialsProviderName)) {
27+
boolean setCorrect = namedClient2Counter.incrementAndGet() == 2;
28+
creds.put("secret-from-vault", setCorrect ? "secret" : "wrong-secret");
29+
} else {
30+
creds.put("secret-from-vault", "secret");
31+
}
32+
1933
return creds;
2034
}
2135

extensions/oidc-client/deployment/src/test/resources/application-oidc-client-credentials.properties

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,17 @@ quarkus.oidc-client.auth-server-url=${quarkus.oidc.auth-server-url}
99
quarkus.oidc-client.client-id=${quarkus.oidc.client-id}
1010
quarkus.oidc-client.credentials.client-secret.provider.name=vault-secret-provider
1111
quarkus.oidc-client.credentials.client-secret.provider.keyring-name=oidc
12-
quarkus.oidc-client.credentials.client-secret.provider.key=secret-from-vault
12+
quarkus.oidc-client.credentials.client-secret.provider.key=secret-from-vault
13+
14+
quarkus.oidc-client.named-1.auth-server-url=${quarkus.oidc.auth-server-url}
15+
quarkus.oidc-client.named-1.client-id=${quarkus.oidc.client-id}
16+
quarkus.oidc-client.named-1.credentials.client-secret.provider.name=${quarkus.oidc-client.credentials.client-secret.provider.name}
17+
quarkus.oidc-client.named-1.credentials.client-secret.provider.keyring-name=named-1-client-secret
18+
quarkus.oidc-client.named-1.credentials.client-secret.provider.key=${quarkus.oidc-client.credentials.client-secret.provider.key}
19+
20+
quarkus.oidc-client.named-2.auth-server-url=${quarkus.oidc.auth-server-url}
21+
quarkus.oidc-client.named-2.client-id=${quarkus.oidc.client-id}
22+
quarkus.oidc-client.named-2.credentials.client-secret.provider.name=${quarkus.oidc-client.credentials.client-secret.provider.name}
23+
quarkus.oidc-client.named-2.credentials.client-secret.provider.keyring-name=named-2-client-secret
24+
quarkus.oidc-client.named-2.credentials.client-secret.provider.key=${quarkus.oidc-client.credentials.client-secret.provider.key}
25+
quarkus.oidc-client.named-2.credentials.client-secret.method=post

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

Lines changed: 117 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,19 +67,21 @@ String operation() {
6767
private final MultiMap tokenGrantParams;
6868
private final MultiMap commonRefreshGrantParams;
6969
private final String grantType;
70-
private final String clientSecretBasicAuthScheme;
7170
private final Key clientJwtKey;
7271
private final boolean jwtBearerAuthentication;
7372
private final OidcClientConfig oidcConfig;
7473
private final Map<OidcEndpoint.Type, List<OidcRequestFilter>> requestFilters;
7574
private final Map<OidcEndpoint.Type, List<OidcResponseFilter>> responseFilters;
7675
private final ClientAssertionProvider clientAssertionProvider;
7776
private volatile boolean closed;
77+
private volatile String clientSecret;
78+
private volatile String clientSecretBasicAuthScheme;
7879

79-
OidcClientImpl(WebClient client, String tokenRequestUri, String tokenRevokeUri, String grantType,
80+
private OidcClientImpl(WebClient client, String tokenRequestUri, String tokenRevokeUri, String grantType,
8081
MultiMap tokenGrantParams, MultiMap commonRefreshGrantParams, OidcClientConfig oidcClientConfig,
8182
Map<OidcEndpoint.Type, List<OidcRequestFilter>> requestFilters,
82-
Map<OidcEndpoint.Type, List<OidcResponseFilter>> responseFilters, Vertx vertx) {
83+
Map<OidcEndpoint.Type, List<OidcResponseFilter>> responseFilters, Vertx vertx,
84+
ClientCredentials clientCredentials) {
8385
this.client = client;
8486
this.tokenRequestUri = tokenRequestUri;
8587
this.tokenRevokeUri = tokenRevokeUri;
@@ -89,9 +91,10 @@ String operation() {
8991
this.oidcConfig = oidcClientConfig;
9092
this.requestFilters = requestFilters;
9193
this.responseFilters = responseFilters;
92-
this.clientSecretBasicAuthScheme = OidcCommonUtils.initClientSecretBasicAuth(oidcClientConfig);
94+
this.clientSecretBasicAuthScheme = clientCredentials.clientSecretBasicAuthScheme;
9395
this.jwtBearerAuthentication = oidcClientConfig.credentials().jwt().source() == Source.BEARER;
94-
this.clientJwtKey = jwtBearerAuthentication ? null : OidcCommonUtils.initClientJwtKey(oidcClientConfig, false);
96+
this.clientJwtKey = jwtBearerAuthentication ? null : clientCredentials.clientJwtKey;
97+
this.clientSecret = clientCredentials.clientSecret;
9598
if (jwtBearerAuthentication && oidcClientConfig.credentials().jwt().tokenPath().isPresent()) {
9699
this.clientAssertionProvider = new ClientAssertionProvider(vertx,
97100
oidcClientConfig.credentials().jwt().tokenPath().get());
@@ -187,12 +190,20 @@ public Uni<Tokens> get() {
187190
});
188191
}
189192

190-
private UniOnItem<HttpResponse<Buffer>> postRequest(
193+
private record PreparedPostRequest(Uni<HttpResponse<Buffer>> postRequest, CredentialsToRetry credentialsToRetry) {
194+
enum CredentialsToRetry {
195+
CLIENT_SECRET,
196+
CLIENT_SECRET_BASIC_AUTH_SCHEME
197+
}
198+
}
199+
200+
private PreparedPostRequest preparePostRequest(
191201
OidcRequestContextProperties requestProps,
192202
OidcEndpoint.Type endpointType, HttpRequest<Buffer> request,
193203
MultiMap formBody,
194204
Map<String, String> additionalGrantParameters,
195205
Operation op) {
206+
PreparedPostRequest.CredentialsToRetry credentialsToRetry = null;
196207
MultiMap body = formBody;
197208
request.putHeader(HttpHeaders.CONTENT_TYPE.toString(),
198209
HttpHeaders.APPLICATION_X_WWW_FORM_URLENCODED.toString());
@@ -204,6 +215,9 @@ private UniOnItem<HttpResponse<Buffer>> postRequest(
204215

205216
if (clientSecretBasicAuthScheme != null) {
206217
request.putHeader(AUTHORIZATION_HEADER, clientSecretBasicAuthScheme);
218+
if (hasClientSecretProvider()) {
219+
credentialsToRetry = PreparedPostRequest.CredentialsToRetry.CLIENT_SECRET_BASIC_AUTH_SCHEME;
220+
}
207221
} else if (jwtBearerAuthentication) {
208222
String clientAssertion = additionalGrantParameters.get(OidcConstants.CLIENT_ASSERTION);
209223
if (clientAssertion == null && clientAssertionProvider != null) {
@@ -245,7 +259,10 @@ private UniOnItem<HttpResponse<Buffer>> postRequest(
245259
} else if (OidcCommonUtils.isClientSecretPostAuthRequired(oidcConfig.credentials())) {
246260
body = !isRefresh(op) ? copyMultiMap(body) : body;
247261
body.set(OidcConstants.CLIENT_ID, oidcConfig.clientId().get());
248-
body.set(OidcConstants.CLIENT_SECRET, OidcCommonUtils.clientSecret(oidcConfig.credentials()));
262+
body.set(OidcConstants.CLIENT_SECRET, clientSecret);
263+
if (hasClientSecretProvider()) {
264+
credentialsToRetry = PreparedPostRequest.CredentialsToRetry.CLIENT_SECRET;
265+
}
249266
} else {
250267
body = !isRefresh(op) ? copyMultiMap(body) : body;
251268
body = copyMultiMap(body).set(OidcConstants.CLIENT_ID, oidcConfig.clientId().get());
@@ -272,7 +289,72 @@ private UniOnItem<HttpResponse<Buffer>> postRequest(
272289
// don't wrap it to avoid information leak
273290
return new OidcClientException("OIDC Server is not available");
274291
});
275-
return response.onItem();
292+
return new PreparedPostRequest(response, credentialsToRetry);
293+
}
294+
295+
private boolean hasClientSecretProvider() {
296+
return oidcConfig.credentials().clientSecret().provider().key().isPresent();
297+
}
298+
299+
private UniOnItem<HttpResponse<Buffer>> postRequest(
300+
OidcRequestContextProperties requestProps,
301+
OidcEndpoint.Type endpointType, HttpRequest<Buffer> request,
302+
MultiMap formBody,
303+
Map<String, String> additionalGrantParameters,
304+
Operation op) {
305+
306+
final MultiMap newFormBody;
307+
boolean hasClientSecretProvider = hasClientSecretProvider();
308+
if (hasClientSecretProvider) {
309+
newFormBody = copyMultiMap(formBody);
310+
} else {
311+
newFormBody = formBody;
312+
}
313+
314+
var preparedRequest = preparePostRequest(requestProps, endpointType, request, newFormBody, additionalGrantParameters,
315+
op);
316+
if (hasClientSecretProvider && preparedRequest.credentialsToRetry != null) {
317+
return preparedRequest.postRequest.flatMap(httpResponse -> {
318+
if (httpResponse.statusCode() == 401) {
319+
// here we need to deal with error responses (like unauthorized_client) possibly caused by
320+
// invalid credentialsToRetry; if credentialsToRetry provider updated credentialsToRetry, we should retry
321+
var credentialsRefresh = switch (preparedRequest.credentialsToRetry) {
322+
case CLIENT_SECRET -> OidcCommonUtils.clientSecret(oidcConfig.credentials())
323+
.map(newClientSecret -> {
324+
if (newClientSecret != null && !newClientSecret.equals(clientSecret)) {
325+
this.clientSecret = newClientSecret;
326+
return true;
327+
}
328+
return false;
329+
});
330+
case CLIENT_SECRET_BASIC_AUTH_SCHEME -> OidcCommonUtils.clientSecret(oidcConfig.credentials())
331+
.map(newClientSecret -> {
332+
var newClientSecretBasicAuthScheme = OidcCommonUtils.initClientSecretBasicAuth(oidcConfig,
333+
newClientSecret);
334+
if (newClientSecretBasicAuthScheme != null
335+
&& !newClientSecretBasicAuthScheme.equals(clientSecretBasicAuthScheme)) {
336+
this.clientSecret = newClientSecret;
337+
this.clientSecretBasicAuthScheme = newClientSecretBasicAuthScheme;
338+
return true;
339+
}
340+
return false;
341+
});
342+
};
343+
344+
return credentialsRefresh.flatMap(credentialsRefreshed -> {
345+
if (Boolean.TRUE.equals(credentialsRefreshed)) {
346+
LOG.debug("HTTP request failed with response status code 401 and the CredentialsProvider"
347+
+ " provided new credentials, retrying the request with new credentials");
348+
return preparePostRequest(requestProps, endpointType, request, formBody,
349+
additionalGrantParameters, op).postRequest;
350+
}
351+
return Uni.createFrom().item(httpResponse);
352+
});
353+
}
354+
return Uni.createFrom().item(httpResponse);
355+
}).onItem();
356+
}
357+
return preparedRequest.postRequest.onItem();
276358
}
277359

278360
private Tokens emitGrantTokens(OidcRequestContextProperties requestProps, HttpResponse<Buffer> resp, Operation op) {
@@ -393,4 +475,31 @@ OidcClientConfig getConfig() {
393475
static boolean isRefresh(Operation op) {
394476
return op == Operation.REFRESH;
395477
}
478+
479+
static Uni<OidcClient> of(WebClient client, String tokenRequestUri, String tokenRevokeUri, String grantType,
480+
MultiMap tokenGrantParams, MultiMap commonRefreshGrantParams, OidcClientConfig oidcClientConfig,
481+
Map<OidcEndpoint.Type, List<OidcRequestFilter>> requestFilters,
482+
Map<OidcEndpoint.Type, List<OidcResponseFilter>> responseFilters, Vertx vertx) {
483+
return OidcCommonUtils.clientSecret(oidcClientConfig.credentials())
484+
.onItem().ifNotNull()
485+
.transform(clientSecret -> new ClientCredentials(clientSecret,
486+
OidcCommonUtils.initClientSecretBasicAuth(oidcClientConfig, clientSecret)))
487+
.onItem().ifNull()
488+
.switchTo(() -> OidcCommonUtils.initClientJwtKey(oidcClientConfig, false).map(ClientCredentials::new))
489+
.onFailure().invoke(t -> LOG.error("Failed to create OidcClientImpl", t))
490+
.map(clientCredentials -> new OidcClientImpl(client, tokenRequestUri, tokenRevokeUri, grantType,
491+
tokenGrantParams,
492+
commonRefreshGrantParams, oidcClientConfig, requestFilters, responseFilters, vertx, clientCredentials));
493+
}
494+
495+
private record ClientCredentials(Key clientJwtKey, String clientSecret, String clientSecretBasicAuthScheme) {
496+
497+
private ClientCredentials(Key clientJwtKey) {
498+
this(clientJwtKey, null, null);
499+
}
500+
501+
private ClientCredentials(String clientSecret, String clientSecretBasicAuthScheme) {
502+
this(null, clientSecret, clientSecretBasicAuthScheme);
503+
}
504+
}
396505
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,10 @@ protected static Uni<OidcClient> createOidcClientUni(OidcClientConfig oidcConfig
130130
}
131131
}
132132
return tokenUrisUni.onItemOrFailure()
133-
.transform(new BiFunction<OidcConfigurationMetadata, Throwable, OidcClient>() {
133+
.transformToUni(new BiFunction<OidcConfigurationMetadata, Throwable, Uni<? extends OidcClient>>() {
134134

135135
@Override
136-
public OidcClient apply(OidcConfigurationMetadata metadata, Throwable t) {
136+
public Uni<OidcClient> apply(OidcConfigurationMetadata metadata, Throwable t) {
137137
if (t != null) {
138138
throw toOidcClientException(getEndpointUrl(oidcConfig), t);
139139
}
@@ -189,7 +189,7 @@ public OidcClient apply(OidcConfigurationMetadata metadata, Throwable t) {
189189
MultiMap commonRefreshGrantParams = new MultiMap(io.vertx.core.MultiMap.caseInsensitiveMultiMap());
190190
setGrantClientParams(oidcConfig, commonRefreshGrantParams, OidcConstants.REFRESH_TOKEN_GRANT);
191191

192-
return new OidcClientImpl(client, metadata.tokenRequestUri, metadata.tokenRevokeUri, grantType,
192+
return OidcClientImpl.of(client, metadata.tokenRequestUri, metadata.tokenRevokeUri, grantType,
193193
tokenGrantParams, commonRefreshGrantParams, oidcConfig, oidcRequestFilters,
194194
oidcResponseFilters, vertx);
195195
}

0 commit comments

Comments
 (0)