From 8736dc6b610543cafcd8e7224cf92c13b594f576 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Mon, 9 Sep 2024 13:45:51 +0100 Subject: [PATCH] Introduce OidcResponseFilter --- ...ecurity-oidc-code-flow-authentication.adoc | 54 +++++++- ...urity-openid-connect-client-reference.adoc | 56 +++++++- ...ty-openid-connect-client-registration.adoc | 123 +++++++++++++++++ .../runtime/OidcClientRegistrationImpl.java | 66 ++++++--- .../OidcClientRegistrationRecorder.java | 21 ++- .../runtime/RegisteredClientImpl.java | 59 +++++--- .../oidc/client/runtime/OidcClientImpl.java | 70 +++++++--- .../client/runtime/OidcClientRecorder.java | 15 ++- .../oidc/common/OidcResponseFilter.java | 27 ++++ .../oidc/common/runtime/OidcCommonUtils.java | 79 ++++++++--- .../oidc/runtime/OidcProviderClient.java | 127 +++++++++++------- .../io/quarkus/oidc/runtime/OidcRecorder.java | 8 +- .../keycloak/TokenEndpointResponseFilter.java | 29 ++++ .../src/main/resources/application.properties | 2 + .../quarkus/it/keycloak/OidcClientTest.java | 13 +- .../oidc-client-registration/pom.xml | 5 + .../ClientRegistrationRequestFilter.java | 27 ++++ .../ClientRegistrationResponseFilter.java | 29 ++++ .../RegisteredClientResponseFilter.java | 27 ++++ .../src/main/resources/application.properties | 11 +- .../keycloak/OidcClientRegistrationTest.java | 63 ++++++++- .../SignedUserInfoResponseFilter.java | 26 ++++ .../it/keycloak/TokenResponseFilter.java | 31 +++++ .../src/main/resources/application.properties | 6 +- .../keycloak/CodeFlowAuthorizationTest.java | 24 +++- 25 files changed, 842 insertions(+), 156 deletions(-) create mode 100644 extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcResponseFilter.java create mode 100644 integration-tests/oidc-client-reactive/src/main/java/io/quarkus/it/keycloak/TokenEndpointResponseFilter.java create mode 100644 integration-tests/oidc-client-registration/src/main/java/io/quarkus/it/keycloak/ClientRegistrationRequestFilter.java create mode 100644 integration-tests/oidc-client-registration/src/main/java/io/quarkus/it/keycloak/ClientRegistrationResponseFilter.java create mode 100644 integration-tests/oidc-client-registration/src/main/java/io/quarkus/it/keycloak/RegisteredClientResponseFilter.java create mode 100644 integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/SignedUserInfoResponseFilter.java create mode 100644 integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/TokenResponseFilter.java diff --git a/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc b/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc index c8330c4b7c90a..e46c5e6e4221d 100644 --- a/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc +++ b/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc @@ -336,7 +336,7 @@ public class OidcTokenRequestCustomizer implements OidcRequestFilter { public void filter(OidcRequestContext requestContext) { OidcConfigurationMetadata metadata = requestContext.contextProperties().get(OidcConfigurationMetadata.class.getName()); <1> // Metadata URI is absolute, request URI value is relative - if (metadata.getTokenUri().endsWith(request.uri())) { <2> + if (metadata.getTokenUri().endsWith(requestContext.request().uri())) { <2> requestContext.request().putHeader("TokenGrantDigest", calculateDigest(requestContext.requestBody().toString())); } } @@ -348,7 +348,7 @@ public class OidcTokenRequestCustomizer implements OidcRequestFilter { <1> Get `OidcConfigurationMetadata`, which contains all supported OIDC endpoint addresses. <2> Use `OidcConfigurationMetadata` to filter requests to the OIDC token endpoint only. -Alternatively, you can use an `@OidcEndpoint` annotation to apply this filter to the token endpoint requests only: +Alternatively, you can use an `@OidcEndpoint` annotation to apply this filter to responses from the OIDC discovery endpoint only: [source,java] ---- @@ -375,6 +375,56 @@ public class OidcDiscoveryRequestCustomizer implements OidcRequestFilter { ---- <1> Restrict this filter to requests targeting the OIDC discovery endpoint only. +`OidcRequestContextProperties` can be used to access request properties. +Currently, you can use a `tenand_id` key to access the OIDC tenant id and a `grant_type` key to access the grant type which the OIDC provider uses to acquire tokens. +The `grant_type` can only be set to either `authorization_code` or `refresh_token` grant type, when requests are made to the token endpoint. It is `null` in all other cases. + +[[code-flow-oidc-response-filters]] +=== OIDC response filters + +You can filter responses from the OIDC providers by registering one or more `OidcResponseFilter` implementations, which can check the response status, headers and body in order to log them or perform other actions. + +You can have a single filter intercepting all the OIDC responses, or use an `@OidcEndpoint` annotation to apply this filter to the specific endpoint responses only. For example: + +[source,java] +---- +package io.quarkus.it.keycloak; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.jboss.logging.Logger; + +import io.quarkus.arc.Unremovable; +import io.quarkus.oidc.common.OidcEndpoint; +import io.quarkus.oidc.common.OidcEndpoint.Type; +import io.quarkus.oidc.common.OidcResponseFilter; +import io.quarkus.oidc.common.runtime.OidcConstants; +import io.quarkus.oidc.runtime.OidcUtils; + +@ApplicationScoped +@Unremovable +@OidcEndpoint(value = Type.TOKEN) <1> +public class TokenEndpointResponseFilter implements OidcResponseFilter { + private static final Logger LOG = Logger.getLogger(TokenResponseFilter.class); + + @Override + public void filter(OidcResponseContext rc) { + String contentType = rc.responseHeaders().get("Content-Type"); <2> + if (contentType.equals("application/json") + && OidcConstants.AUTHORIZATION_CODE.equals(rc.requestProperties().get(OidcConstants.GRANT_TYPE)) <3> + && "code-flow-user-info-cached-in-idtoken".equals(rc.requestProperties().get(OidcUtils.TENANT_ID_ATTRIBUTE)) <3> + && rc.responseBody().toJsonObject().containsKey("id_token")) { <4> + LOG.debug("Authorization code completed for tenant 'code-flow-user-info-cached-in-idtoken'"); + } + } +} + +---- +<1> Restrict this filter to requests targeting the OIDC token endpoint only. +<2> Check the response `Content-Type` header. +<3> Use `OidcRequestContextProperties` request properties to check only an `authorization_code` token grant response for the `code-flow-user-info-cached-in-idtoken` tenant. +<4> Confirm the response JSON contains an `id_token` property. + === Redirecting to and from the OIDC provider When a user is redirected to the OIDC provider to authenticate, the redirect URL includes a `redirect_uri` query parameter, which indicates to the provider where the user has to be redirected to when the authentication is complete. diff --git a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc index 69c2fa0db1252..a87accec7d014 100644 --- a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc @@ -1107,7 +1107,9 @@ quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientRecorder".min-lev [[oidc-client-ref-oidc-request-filters]] == OIDC request filters -You can filter OIDC requests made by Quarkus to the OIDC provider by registering one or more `OidcRequestFilter` implementations, which can update or add new request headers. For example, a filter can analyze the request body and add its digest as a new header value: +You can filter OIDC requests made by OIDC client to the OIDC provider by registering one or more `OidcRequestFilter` implementations, which can update or add new request headers, or analyze the request body. + +You can have a single filter intercepting requests to all OIDC provider endpoints, or use an `@OidcEndpoint` annotation to apply this filter to requests to specific endpoints only. For example: [source,java] ---- @@ -1116,11 +1118,12 @@ package io.quarkus.it.keycloak; import jakarta.enterprise.context.ApplicationScoped; import io.quarkus.arc.Unremovable; -import io.quarkus.oidc.common.OidcRequestContext; +import io.quarkus.oidc.common.OidcEndpoint; import io.quarkus.oidc.common.OidcRequestFilter; import io.vertx.core.http.HttpMethod; @ApplicationScoped +@OidcEndpoint(value = Type.TOKEN) @Unremovable public class OidcRequestCustomizer implements OidcRequestFilter { @@ -1128,7 +1131,7 @@ public class OidcRequestCustomizer implements OidcRequestFilter { public void filter(OidcRequestContext requestContext) { HttpMethod method = requestContext.request().method(); String uri = requestContext.request().uri(); - if (method == HttpMethod.POST && uri.endsWith("/service") && requestContext.requestBody() != null) { + if (method == HttpMethod.POST && uri.endsWith("/token") && requestContext.requestBody() != null) { requestContext.request().putHeader("Digest", calculateDigest(requestContext.requestBody().toString())); } } @@ -1139,6 +1142,53 @@ public class OidcRequestCustomizer implements OidcRequestFilter { } ---- +`OidcRequestContextProperties` can be used to access request properties. +Currently, you can use a `client_id` key to access the client tenant id and a `grant_type` key to access the grant type which the OIDC client uses to acquire tokens. + +[[oidc-client-ref-oidc-response-filters]] +== OIDC response filters + +You can filter responses to the OIDC client requests by registering one or more `OidcResponseFilter` implementations, which can check the response status, headers and body, in order to log them or perform other actions. + +You can have a single filter intercepting responses to all OIDC client requests, or use an `@OidcEndpoint` annotation to apply this filter to the responses to the specific OIDC client requests only. For example: + +[source,java] +---- +package io.quarkus.it.keycloak; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.jboss.logging.Logger; + +import io.quarkus.arc.Unremovable; +import io.quarkus.oidc.common.OidcEndpoint; +import io.quarkus.oidc.common.OidcEndpoint.Type; +import io.quarkus.oidc.common.OidcResponseFilter; +import io.quarkus.oidc.common.runtime.OidcConstants; + +@ApplicationScoped +@Unremovable +@OidcEndpoint(value = Type.TOKEN) <1> +public class TokenEndpointResponseFilter implements OidcResponseFilter { + private static final Logger LOG = Logger.getLogger(TokenEndpointResponseFilter.class); + + @Override + public void filter(OidcResponseContext rc) { + String contentType = rc.responseHeaders().get("Content-Type"); <2> + if (contentType.equals("application/json") + && "refresh_token".equals(rc.requestProperties().get(OidcConstants.GRANT_TYPE)) <3> + && rc.responseBody().toJsonObject().containsKey("refresh_token")) { <4> + LOG.debug("Tokens have been refreshed"); + } + } + +} +---- +<1> Restrict this filter to requests targeting the OIDC token endpoint only. +<2> Check the response `Content-Type` header. +<3> Use `OidcRequestContextProperties` request properties to confirm it is a `refresh_grant` token grant response. +<4> Confirm the response JSON contains a `refresh_token` property. + [[token-propagation-rest]] == Token Propagation for Quarkus REST diff --git a/docs/src/main/asciidoc/security-openid-connect-client-registration.adoc b/docs/src/main/asciidoc/security-openid-connect-client-registration.adoc index 17e8134a02cd4..e5fe4efd02415 100644 --- a/docs/src/main/asciidoc/security-openid-connect-client-registration.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-client-registration.adoc @@ -496,6 +496,129 @@ public class CustomTenantConfigResolver implements TenantConfigResolver { If you register clients dynamically, on demand, as described in the <> section, the problem of the duplicate client registration should not arise. You can persist the already registered client's registration URI and registration token if necessary though and check them too to avoid any duplicate reservation risk. +[[oidc-client-registration-oidc-request-filters]] +== OIDC request filters + +You can filter OIDC client registration and registered client requests registering one or more `OidcRequestFilter` implementations, which can update or add new request headers. For example, a filter can analyze the request body and add its digest as a new header value: + +You can have a single filter intercepting all the OIDC registration and registered client requests, or use an `@OidcEndpoint` annotation to apply this filter to either OIDC registration or registered client endpoint responses only. For example: + +[source,java] +---- +package io.quarkus.it.keycloak; + +import org.jboss.logging.Logger; + +import io.quarkus.arc.Unremovable; +import io.quarkus.oidc.common.OidcEndpoint; +import io.quarkus.oidc.common.OidcEndpoint.Type; +import io.quarkus.oidc.common.OidcRequestFilter; +import io.vertx.core.json.JsonObject; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +@Unremovable +@OidcEndpoint(value = Type.CLIENT_REGISTRATION) <1> +public class ClientRegistrationRequestFilter implements OidcRequestFilter { + private static final Logger LOG = Logger.getLogger(ClientRegistrationRequestFilter.class); + + @Override + public void filter(OidcRequestContext rc) { + JsonObject body = rc.requestBody().toJsonObject(); + if ("Default Client".equals(body.getString("client_name"))) { <2> + LOG.debug("'Default Client' registration request"); + } + } + +} +---- +<1> Restrict this filter to requests targeting the OIDC client registration endpoint only. +<2> Check the 'client_name' property in the request metadata JSON. + +`OidcRequestContextProperties` can be used to access request properties. +Currently, you can use a `client_id` key to access the client tenant id and a `grant_type` key to access the grant type which the OIDC client uses to acquire tokens. + +[[oidc-client-registration-oidc-response-filters]] +== OIDC response filters + +You can filter responses to OIDC client registration and registered client requests by registering one or more `OidcResponseFilter` implementations, which can check the response status, headers and body in order to log them or perform other actions. + +You can have a single filter intercepting responses to all the OIDC registration and registered client requests, or use an `@OidcEndpoint` annotation to apply this filter to responses from either OIDC registration or registered client endpoint only. For example: + +[source,java] +---- +package io.quarkus.it.keycloak; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.jboss.logging.Logger; + +import io.quarkus.arc.Unremovable; +import io.quarkus.oidc.common.OidcEndpoint; +import io.quarkus.oidc.common.OidcEndpoint.Type; +import io.quarkus.oidc.common.OidcResponseFilter; +import io.vertx.core.json.JsonObject; + +@ApplicationScoped +@Unremovable +@OidcEndpoint(value = Type.CLIENT_REGISTRATION) <1> +public class ClientRegistrationResponseFilter implements OidcResponseFilter { + private static final Logger LOG = Logger.getLogger(ClientRegistrationResponseFilter.class); + + @Override + public void filter(OidcResponseContext rc) { + String contentType = rc.responseHeaders().get("Content-Type"); <2> + JsonObject body = rc.responseBody().toJsonObject(); + if (contentType.startsWith("application/json") + && "Default Client".equals(body.getString("client_name"))) { <3> + LOG.debug("'Default Client' has been registered"); + } + } + +} + +---- +<1> Restrict this filter to requests targeting the OIDC client registration endpoint only. +<2> Check the response `Content-Type` header. +<3> Check the 'client_name' property in the response metadata JSON. + +or + +[source,java] +---- +package io.quarkus.it.keycloak; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.jboss.logging.Logger; + +import io.quarkus.arc.Unremovable; +import io.quarkus.oidc.common.OidcEndpoint; +import io.quarkus.oidc.common.OidcEndpoint.Type; +import io.quarkus.oidc.common.OidcResponseFilter; + +@ApplicationScoped +@Unremovable +@OidcEndpoint(value = Type.REGISTERED_CLIENT) <1> +public class RegisteredClientResponseFilter implements OidcResponseFilter { + private static final Logger LOG = Logger.getLogger(RegisteredClientResponseFilter.class); + + @Override + public void filter(OidcResponseContext rc) { + String contentType = rc.responseHeaders().get("Content-Type"); <2> + if (contentType.startsWith("application/json") + && "Default Client Updated".equals(rc.responseBody().toJsonObject().getString("client_name"))) { <3> + LOG.debug("Registered 'Default Client' has had its name updated to 'Default Client Updated'"); + } + } + +} + +---- +<1> Restrict this filter to requests targeting the registered OIDC client endpoint only. +<2> Check the response `Content-Type` header. +<3> Confirm the client name property was updated. + [[configuration-reference]] == Configuration reference diff --git a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationImpl.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationImpl.java index 649df2084243a..be4e72d3e87f3 100644 --- a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationImpl.java +++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationImpl.java @@ -22,6 +22,7 @@ import io.quarkus.oidc.common.OidcRequestContextProperties; import io.quarkus.oidc.common.OidcRequestFilter; import io.quarkus.oidc.common.OidcRequestFilter.OidcRequestContext; +import io.quarkus.oidc.common.OidcResponseFilter; import io.quarkus.oidc.common.runtime.OidcCommonUtils; import io.quarkus.oidc.common.runtime.OidcConstants; import io.smallrye.mutiny.Multi; @@ -45,19 +46,22 @@ public class OidcClientRegistrationImpl implements OidcClientRegistration { private final long connectionDelayInMillisecs; private final String registrationUri; private final OidcClientRegistrationConfig oidcConfig; - private final Map> filters; + private final Map> requestFilters; + private final Map> responseFilters; private final RegisteredClient registeredClient; private volatile boolean closed; public OidcClientRegistrationImpl(WebClient client, long connectionDelayInMillisecs, String registrationUri, OidcClientRegistrationConfig oidcConfig, RegisteredClient registeredClient, - Map> oidcRequestFilters) { + Map> oidcRequestFilters, + Map> oidcResponseFilters) { this.client = client; this.connectionDelayInMillisecs = connectionDelayInMillisecs; this.registrationUri = registrationUri; this.oidcConfig = oidcConfig; - this.filters = oidcRequestFilters; + this.requestFilters = oidcRequestFilters; + this.responseFilters = oidcResponseFilters; this.registeredClient = registeredClient; } @@ -75,7 +79,7 @@ public Uni registeredClient() { return Uni.createFrom().nullItem(); } else { return registerClient(client, registrationUri, - oidcConfig, filters, metadata.getMetadataString()) + oidcConfig, requestFilters, responseFilters, metadata.getMetadataString()) .onFailure(OidcCommonUtils.oidcEndpointNotAvailable()) .retry() .withBackOff(OidcCommonUtils.CONNECTION_BACKOFF_DURATION, @@ -89,22 +93,28 @@ public Uni registeredClient() { public Uni registerClient(ClientMetadata metadata) { LOG.debugf("Register client metadata: %s", metadata.getMetadataString()); checkClosed(); - return postRequest(client, registrationUri, oidcConfig, filters, metadata.getMetadataString()) - .transform(resp -> newRegisteredClient(resp, client, registrationUri, oidcConfig, filters)); + OidcRequestContextProperties requestProps = getRequestProps(); + return postRequest(requestProps, client, registrationUri, oidcConfig, requestFilters, metadata.getMetadataString()) + .transform(resp -> newRegisteredClient(resp, client, registrationUri, oidcConfig, requestFilters, + responseFilters, requestProps)); } @Override public Multi registerClients(List metadataList) { LOG.debugf("Register clients"); checkClosed(); + OidcRequestContextProperties requestProps = getRequestProps(); return Multi.createFrom().emitter(new Consumer>() { @Override public void accept(MultiEmitter multiEmitter) { try { AtomicInteger emitted = new AtomicInteger(); for (ClientMetadata metadata : metadataList) { - postRequest(client, registrationUri, oidcConfig, filters, metadata.getMetadataString()) - .transform(resp -> newRegisteredClient(resp, client, registrationUri, oidcConfig, filters)) + postRequest(requestProps, client, registrationUri, oidcConfig, requestFilters, + metadata.getMetadataString()) + .transform( + resp -> newRegisteredClient(resp, client, registrationUri, oidcConfig, requestFilters, + responseFilters, requestProps)) .subscribe().with(new Consumer() { @Override public void accept(RegisteredClient client) { @@ -122,16 +132,25 @@ public void accept(RegisteredClient client) { }); } + private OidcRequestContextProperties getRequestProps() { + return requestFilters.isEmpty() && responseFilters.isEmpty() ? null : new OidcRequestContextProperties(); + } + static Uni registerClient(WebClient client, String registrationUri, OidcClientRegistrationConfig oidcConfig, - Map> filters, + Map> requestFilters, + Map> responseFilters, String clientRegJson) { - return postRequest(client, registrationUri, oidcConfig, filters, clientRegJson) - .transform(resp -> newRegisteredClient(resp, client, registrationUri, oidcConfig, filters)); + OidcRequestContextProperties requestProps = requestFilters == null && responseFilters.isEmpty() ? null + : new OidcRequestContextProperties(); + return postRequest(requestProps, client, registrationUri, oidcConfig, requestFilters, clientRegJson) + .transform(resp -> newRegisteredClient(resp, client, registrationUri, oidcConfig, requestFilters, + responseFilters, requestProps)); } - static UniOnItem> postRequest(WebClient client, String registrationUri, + static UniOnItem> postRequest(OidcRequestContextProperties requestProps, + WebClient client, String registrationUri, OidcClientRegistrationConfig oidcConfig, Map> filters, String clientRegJson) { HttpRequest request = client.postAbs(registrationUri); @@ -142,7 +161,7 @@ static UniOnItem> postRequest(WebClient client, String regi } // Retry up to three times with a one-second delay between the retries if the connection is closed Buffer buffer = Buffer.buffer(clientRegJson); - Uni> response = filter(request, filters, buffer).sendBuffer(buffer) + Uni> response = filterHttpRequest(requestProps, request, filters, buffer).sendBuffer(buffer) .onFailure(ConnectException.class) .retry() .atMost(oidcConfig.connectionRetryCount) @@ -154,11 +173,12 @@ static UniOnItem> postRequest(WebClient client, String regi return response.onItem(); } - static private HttpRequest filter(HttpRequest request, Map> filters, + static private HttpRequest filterHttpRequest(OidcRequestContextProperties requestProps, + HttpRequest request, + Map> filters, Buffer body) { if (!filters.isEmpty()) { - OidcRequestContextProperties props = new OidcRequestContextProperties(); - OidcRequestContext context = new OidcRequestContext(request, body, props); + OidcRequestContext context = new OidcRequestContext(request, body, requestProps); for (OidcRequestFilter filter : OidcCommonUtils.getMatchingOidcRequestFilters(filters, OidcEndpoint.Type.CLIENT_REGISTRATION)) { filter.filter(context); @@ -169,9 +189,13 @@ static private HttpRequest filter(HttpRequest request, Map resp, WebClient client, String registrationUri, OidcClientRegistrationConfig oidcConfig, - Map> filters) { + Map> requestFilters, + Map> responseFilters, + OidcRequestContextProperties requestProps) { + Buffer buffer = resp.body(); + OidcCommonUtils.filterHttpResponse(requestProps, resp, buffer, responseFilters, OidcEndpoint.Type.CLIENT_REGISTRATION); if (resp.statusCode() == 200 || resp.statusCode() == 201) { - JsonObject json = resp.bodyAsJsonObject(); + JsonObject json = buffer.toJsonObject(); LOG.debugf("Client has been succesfully registered: %s", json.toString()); String registrationClientUri = (String) json.remove(OidcConstants.REGISTRATION_CLIENT_URI); @@ -179,10 +203,10 @@ static private RegisteredClient newRegisteredClient(HttpResponse resp, ClientMetadata metadata = new ClientMetadata(json.toString()); - return new RegisteredClientImpl(client, oidcConfig, filters, metadata, + return new RegisteredClientImpl(client, oidcConfig, requestFilters, responseFilters, metadata, registrationClientUri, registrationToken); } else { - String errorMessage = resp.bodyAsString(); + String errorMessage = buffer.toString(); LOG.errorf("Client registeration has failed: status: %d, error message: %s", resp.statusCode(), errorMessage); throw new OidcClientRegistrationException(errorMessage); @@ -193,7 +217,7 @@ static private RegisteredClient newRegisteredClient(HttpResponse resp, public Uni readClient(String registrationUri, String registrationToken) { @SuppressWarnings("resource") RegisteredClient newClient = new RegisteredClientImpl(client, oidcConfig, - filters, createMetadata(oidcConfig.metadata), registrationUri, registrationToken); + requestFilters, responseFilters, createMetadata(oidcConfig.metadata), registrationUri, registrationToken); return newClient.read(); } diff --git a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationRecorder.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationRecorder.java index f9d4fbf2e7b68..58c4488115f9a 100644 --- a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationRecorder.java +++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationRecorder.java @@ -19,6 +19,7 @@ import io.quarkus.oidc.common.OidcEndpoint; import io.quarkus.oidc.common.OidcRequestContextProperties; import io.quarkus.oidc.common.OidcRequestFilter; +import io.quarkus.oidc.common.OidcResponseFilter; import io.quarkus.oidc.common.runtime.OidcCommonUtils; import io.quarkus.oidc.common.runtime.OidcTlsSupport; import io.quarkus.runtime.annotations.Recorder; @@ -123,7 +124,7 @@ public static Uni createOidcClientRegistrationUni(OidcCl WebClient client = WebClient.create(vertx, options); Map> oidcRequestFilters = OidcCommonUtils.getOidcRequestFilters(); - + Map> oidcResponseFilters = OidcCommonUtils.getOidcResponseFilters(); Uni clientRegConfigUni = null; if (OidcCommonUtils.isAbsoluteUrl(oidcConfig.registrationPath)) { clientRegConfigUni = Uni.createFrom().item( @@ -135,7 +136,8 @@ public static Uni createOidcClientRegistrationUni(OidcCl .item(new OidcConfigurationMetadata( OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.registrationPath))); } else { - clientRegConfigUni = discoverRegistrationUri(client, oidcRequestFilters, authServerUriString.toString(), vertx, + clientRegConfigUni = discoverRegistrationUri(client, oidcRequestFilters, oidcResponseFilters, + authServerUriString.toString(), vertx, oidcConfig); } } @@ -164,7 +166,8 @@ public Uni apply(OidcConfigurationMetadata metadata, Thr metadata.clientRegistrationUri, oidcConfig, null, - oidcRequestFilters)); + oidcRequestFilters, + oidcResponseFilters)); } else if (clientMetadata.getJsonObject().isEmpty()) { LOG.debugf("%s client registration is skipped because its metadata is not configured", oidcConfig.id.orElse(DEFAULT_ID)); @@ -173,11 +176,12 @@ public Uni apply(OidcConfigurationMetadata metadata, Thr metadata.clientRegistrationUri, oidcConfig, null, - oidcRequestFilters)); + oidcRequestFilters, + oidcResponseFilters)); } else { return OidcClientRegistrationImpl.registerClient(client, metadata.clientRegistrationUri, - oidcConfig, oidcRequestFilters, clientMetadata.getMetadataString()) + oidcConfig, oidcRequestFilters, oidcResponseFilters, clientMetadata.getMetadataString()) .onFailure(OidcCommonUtils.oidcEndpointNotAvailable()) .retry() .withBackOff(OidcCommonUtils.CONNECTION_BACKOFF_DURATION, @@ -202,7 +206,8 @@ public OidcClientRegistration apply(RegisteredClient r, Throwable t2) { metadata.clientRegistrationUri, oidcConfig, registeredClient, - oidcRequestFilters); + oidcRequestFilters, + oidcResponseFilters); } }); } @@ -216,10 +221,12 @@ private static String getEndpointUrl(OidcClientRegistrationConfig oidcConfig) { private static Uni discoverRegistrationUri(WebClient client, Map> oidcRequestFilters, + Map> oidcResponseFilters, String authServerUrl, io.vertx.mutiny.core.Vertx vertx, OidcClientRegistrationConfig oidcConfig) { final long connectionDelayInMillisecs = OidcCommonUtils.getConnectionDelayInMillis(oidcConfig); return OidcCommonUtils - .discoverMetadata(client, oidcRequestFilters, new OidcRequestContextProperties(), authServerUrl, + .discoverMetadata(client, oidcRequestFilters, new OidcRequestContextProperties(), + oidcResponseFilters, authServerUrl, connectionDelayInMillisecs, vertx, oidcConfig.useBlockingDnsLookup) .onItem().transform(json -> new OidcConfigurationMetadata(json.getString("registration_endpoint"))); diff --git a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/RegisteredClientImpl.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/RegisteredClientImpl.java index e7ee633b228d0..0288735390dc7 100644 --- a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/RegisteredClientImpl.java +++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/RegisteredClientImpl.java @@ -21,6 +21,7 @@ import io.quarkus.oidc.common.OidcRequestContextProperties; import io.quarkus.oidc.common.OidcRequestFilter; import io.quarkus.oidc.common.OidcRequestFilter.OidcRequestContext; +import io.quarkus.oidc.common.OidcResponseFilter; import io.quarkus.oidc.common.runtime.OidcCommonUtils; import io.quarkus.oidc.common.runtime.OidcConstants; import io.smallrye.mutiny.Uni; @@ -39,24 +40,28 @@ public class RegisteredClientImpl implements RegisteredClient { //https://datatracker.ietf.org/doc/html/rfc7592.html#section-2.2 private static final Set PRIVATE_PROPERTIES = Set.of(OidcConstants.CLIENT_METADATA_SECRET_EXPIRES_AT, OidcConstants.CLIENT_METADATA_ID_ISSUED_AT); + private static final OidcRequestContextProperties DEFAULT_REQUEST_PROPS = new OidcRequestContextProperties(); private final WebClient client; private final OidcClientRegistrationConfig oidcConfig; private final String registrationClientUri; private final String registrationToken; private final ClientMetadata registeredMetadata; - private final Map> filters; + private final Map> requestFilters; + private final Map> responseFilters; private volatile boolean closed; public RegisteredClientImpl(WebClient client, OidcClientRegistrationConfig oidcConfig, Map> oidcRequestFilters, + Map> oidcResponseFilters, ClientMetadata registeredMetadata, String registrationClientUri, String registrationToken) { this.client = client; this.oidcConfig = oidcConfig; this.registrationClientUri = registrationClientUri; this.registrationToken = registrationToken; this.registeredMetadata = registeredMetadata; - this.filters = oidcRequestFilters; + this.requestFilters = oidcRequestFilters; + this.responseFilters = oidcResponseFilters; } @Override @@ -71,8 +76,9 @@ public Uni read() { checkClientRequestUri(); HttpRequest request = client.getAbs(registrationClientUri); request.putHeader(HttpHeaders.ACCEPT.toString(), APPLICATION_JSON); - return makeRequest(request, Buffer.buffer()) - .transform(resp -> newRegisteredClient(resp)); + OidcRequestContextProperties requestProps = getRequestProps(); + return makeRequest(requestProps, request, Buffer.buffer()) + .transform(resp -> newRegisteredClient(resp, requestProps)); } @Override @@ -118,17 +124,22 @@ public Uni update(ClientMetadata newMetadata) { HttpRequest request = client.putAbs(registrationClientUri); request.putHeader(HttpHeaders.CONTENT_TYPE.toString(), APPLICATION_JSON); request.putHeader(HttpHeaders.ACCEPT.toString(), APPLICATION_JSON); - return makeRequest(request, Buffer.buffer(json.toString())) - .transform(resp -> newRegisteredClient(resp)); + OidcRequestContextProperties requestProps = getRequestProps(); + return makeRequest(requestProps, request, Buffer.buffer(json.toString())) + .transform(resp -> newRegisteredClient(resp, requestProps)); } @Override public Uni delete() { checkClosed(); checkClientRequestUri(); + OidcRequestContextProperties requestProps = getRequestProps(); + return makeRequest(requestProps, client.deleteAbs(registrationClientUri), Buffer.buffer()) + .transformToUni(resp -> deleteResponse(resp, requestProps)); + } - return makeRequest(client.deleteAbs(registrationClientUri), Buffer.buffer()) - .transformToUni(resp -> deleteResponse(resp)); + private OidcRequestContextProperties getRequestProps() { + return requestFilters.isEmpty() ? DEFAULT_REQUEST_PROPS : new OidcRequestContextProperties(); } @Override @@ -143,12 +154,13 @@ public void close() throws IOException { } } - private UniOnItem> makeRequest(HttpRequest request, Buffer buffer) { + private UniOnItem> makeRequest(OidcRequestContextProperties requestProps, HttpRequest request, + Buffer buffer) { if (registrationToken != null) { request.putHeader(AUTHORIZATION_HEADER, OidcConstants.BEARER_SCHEME + " " + registrationToken); } // Retry up to three times with a one-second delay between the retries if the connection is closed - Uni> response = filter(request, buffer).sendBuffer(buffer) + Uni> response = filterHttpRequest(requestProps, request, buffer).sendBuffer(buffer) .onFailure(ConnectException.class) .retry() .atMost(oidcConfig.connectionRetryCount) @@ -160,11 +172,11 @@ private UniOnItem> makeRequest(HttpRequest request, return response.onItem(); } - private HttpRequest filter(HttpRequest request, Buffer body) { - if (!filters.isEmpty()) { - OidcRequestContextProperties props = new OidcRequestContextProperties(); - OidcRequestContext context = new OidcRequestContext(request, body, props); - for (OidcRequestFilter filter : OidcCommonUtils.getMatchingOidcRequestFilters(filters, + private HttpRequest filterHttpRequest(OidcRequestContextProperties requestProps, HttpRequest request, + Buffer body) { + if (!requestFilters.isEmpty()) { + OidcRequestContext context = new OidcRequestContext(request, body, requestProps); + for (OidcRequestFilter filter : OidcCommonUtils.getMatchingOidcRequestFilters(requestFilters, OidcEndpoint.Type.REGISTERED_CLIENT)) { filter.filter(context); } @@ -172,31 +184,36 @@ private HttpRequest filter(HttpRequest request, Buffer body) { return request; } - private RegisteredClient newRegisteredClient(HttpResponse resp) { + private RegisteredClient newRegisteredClient(HttpResponse resp, OidcRequestContextProperties requestProps) { + Buffer buffer = resp.body(); + OidcCommonUtils.filterHttpResponse(requestProps, resp, buffer, responseFilters, OidcEndpoint.Type.REGISTERED_CLIENT); if (resp.statusCode() >= 200 && resp.statusCode() < 300) { - io.vertx.core.json.JsonObject json = resp.bodyAsJsonObject(); + io.vertx.core.json.JsonObject json = buffer.toJsonObject(); LOG.debugf("Client metadata has been succesfully updated: %s", json.toString()); String newRegistrationClientUri = (String) json.remove(OidcConstants.REGISTRATION_CLIENT_URI); String newRegistrationToken = (String) json.remove(OidcConstants.REGISTRATION_ACCESS_TOKEN); - return new RegisteredClientImpl(client, oidcConfig, filters, new ClientMetadata(json.toString()), + return new RegisteredClientImpl(client, oidcConfig, requestFilters, responseFilters, + new ClientMetadata(json.toString()), (newRegistrationClientUri != null ? newRegistrationClientUri : registrationClientUri), (newRegistrationToken != null ? newRegistrationToken : registrationToken)); } else { - String errorMessage = resp.bodyAsString(); + String errorMessage = buffer.toString(); LOG.debugf("Client configuration update has failed: status: %d, error message: %s", resp.statusCode(), errorMessage); throw new OidcClientRegistrationException(errorMessage); } } - private Uni deleteResponse(HttpResponse resp) { + private Uni deleteResponse(HttpResponse resp, OidcRequestContextProperties requestProps) { + Buffer buffer = resp.body(); + OidcCommonUtils.filterHttpResponse(requestProps, resp, buffer, responseFilters, OidcEndpoint.Type.REGISTERED_CLIENT); if (resp.statusCode() == 200) { LOG.debug("Client has been succesfully deleted"); return Uni.createFrom().voidItem(); } else { - String errorMessage = resp.bodyAsString(); + String errorMessage = buffer.toString(); LOG.debugf("Client delete request has failed: status: %d, error message: %s", resp.statusCode(), errorMessage); return Uni.createFrom().voidItem(); diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientImpl.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientImpl.java index b0191acd71417..dc63d9ee40a09 100644 --- a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientImpl.java +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientImpl.java @@ -6,6 +6,7 @@ import java.security.Key; import java.time.Instant; import java.util.Base64; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Supplier; @@ -21,6 +22,7 @@ import io.quarkus.oidc.common.OidcRequestContextProperties; import io.quarkus.oidc.common.OidcRequestFilter; import io.quarkus.oidc.common.OidcRequestFilter.OidcRequestContext; +import io.quarkus.oidc.common.OidcResponseFilter; import io.quarkus.oidc.common.runtime.OidcClientCommonConfig.Credentials.Jwt.Source; import io.quarkus.oidc.common.runtime.OidcCommonUtils; import io.quarkus.oidc.common.runtime.OidcConstants; @@ -52,12 +54,14 @@ public class OidcClientImpl implements OidcClient { private final Key clientJwtKey; private final boolean jwtBearerAuthentication; private final OidcClientConfig oidcConfig; - private final Map> filters; + private final Map> requestFilters; + private final Map> responseFilters; private volatile boolean closed; public OidcClientImpl(WebClient client, String tokenRequestUri, String tokenRevokeUri, String grantType, MultiMap tokenGrantParams, MultiMap commonRefreshGrantParams, OidcClientConfig oidcClientConfig, - Map> filters) { + Map> requestFilters, + Map> responseFilters) { this.client = client; this.tokenRequestUri = tokenRequestUri; this.tokenRevokeUri = tokenRevokeUri; @@ -65,7 +69,8 @@ public OidcClientImpl(WebClient client, String tokenRequestUri, String tokenRevo this.commonRefreshGrantParams = commonRefreshGrantParams; this.grantType = grantType; this.oidcConfig = oidcClientConfig; - this.filters = filters; + this.requestFilters = requestFilters; + this.responseFilters = responseFilters; this.clientSecretBasicAuthScheme = OidcCommonUtils.initClientSecretBasicAuth(oidcClientConfig); this.jwtBearerAuthentication = oidcClientConfig.credentials.jwt.source == Source.BEARER; this.clientJwtKey = jwtBearerAuthentication ? null : OidcCommonUtils.initClientJwtKey(oidcClientConfig, false); @@ -98,12 +103,15 @@ public Uni revokeAccessToken(String accessToken, Map ad if (accessToken == null) { throw new OidcClientException("Access token is null"); } + OidcRequestContextProperties requestProps = getRequestProps(null); + if (tokenRevokeUri != null) { MultiMap tokenRevokeParams = new MultiMap(io.vertx.core.MultiMap.caseInsensitiveMultiMap()); tokenRevokeParams.set(OidcConstants.REVOCATION_TOKEN, accessToken); - return postRequest(OidcEndpoint.Type.TOKEN_REVOCATION, client.postAbs(tokenRevokeUri), tokenRevokeParams, + return postRequest(requestProps, OidcEndpoint.Type.TOKEN_REVOCATION, client.postAbs(tokenRevokeUri), + tokenRevokeParams, additionalParameters, false) - .transform(resp -> toRevokeResponse(resp)); + .transform(resp -> toRevokeResponse(requestProps, resp)); } else { LOG.debugf("%s OidcClient can not revoke the access token because the revocation endpoint URL is not set"); return Uni.createFrom().item(false); @@ -111,30 +119,50 @@ public Uni revokeAccessToken(String accessToken, Map ad } - private Boolean toRevokeResponse(HttpResponse resp) { + private OidcRequestContextProperties getRequestProps(String grantType) { + if (requestFilters.isEmpty() && responseFilters.isEmpty()) { + return null; + } + Map props = new HashMap<>(); + props.put(CLIENT_ID_ATTRIBUTE, oidcConfig.getId().orElse(DEFAULT_OIDC_CLIENT_ID)); + if (grantType != null) { + props.put(OidcConstants.GRANT_TYPE, grantType); + } + return new OidcRequestContextProperties(props); + } + + private Boolean toRevokeResponse(OidcRequestContextProperties requestProps, HttpResponse resp) { // Per RFC7009, 200 is returned if a token has been revoked successfully or if the client submitted an // invalid token, https://datatracker.ietf.org/doc/html/rfc7009#section-2.2. // 503 is at least theoretically possible if the OIDC server declines and suggests to Retry-After some period of time. // However this period of time can be set to unpredictable value. + Buffer buffer = resp.body(); + OidcCommonUtils.filterHttpResponse(requestProps, resp, buffer, responseFilters, OidcEndpoint.Type.TOKEN_REVOCATION); return resp.statusCode() == 503 ? false : true; } - private Uni getJsonResponse(OidcEndpoint.Type endpointType, MultiMap formBody, + private Uni getJsonResponse( + OidcEndpoint.Type endpointType, MultiMap formBody, Map additionalGrantParameters, boolean refresh) { //Uni needs to be lazy by default, we don't send the request unless //something has subscribed to it. This is important for the CAS state //management in TokensHelper + String currentGrantType = refresh ? OidcConstants.REFRESH_TOKEN_GRANT : grantType; + final OidcRequestContextProperties requestProps = getRequestProps(currentGrantType); return Uni.createFrom().deferred(new Supplier>() { @Override public Uni get() { - return postRequest(endpointType, client.postAbs(tokenRequestUri), formBody, additionalGrantParameters, refresh) - .transform(resp -> emitGrantTokens(resp, refresh)); + return postRequest(requestProps, endpointType, client.postAbs(tokenRequestUri), formBody, + additionalGrantParameters, refresh) + .transform(resp -> emitGrantTokens(requestProps, resp, refresh)); } }); } - private UniOnItem> postRequest(OidcEndpoint.Type endpointType, HttpRequest request, + private UniOnItem> postRequest( + OidcRequestContextProperties requestProps, + OidcEndpoint.Type endpointType, HttpRequest request, MultiMap formBody, Map additionalGrantParameters, boolean refresh) { @@ -196,7 +224,7 @@ private UniOnItem> postRequest(OidcEndpoint.Type endpointTy } // Retry up to three times with a one-second delay between the retries if the connection is closed Buffer buffer = OidcCommonUtils.encodeForm(body); - Uni> response = filter(endpointType, request, buffer).sendBuffer(buffer) + Uni> response = filterHttpRequest(requestProps, endpointType, request, buffer).sendBuffer(buffer) .onFailure(ConnectException.class) .retry() .atMost(oidcConfig.connectionRetryCount) @@ -208,10 +236,12 @@ private UniOnItem> postRequest(OidcEndpoint.Type endpointTy return response.onItem(); } - private Tokens emitGrantTokens(HttpResponse resp, boolean refresh) { + private Tokens emitGrantTokens(OidcRequestContextProperties requestProps, HttpResponse resp, boolean refresh) { + Buffer buffer = resp.body(); + OidcCommonUtils.filterHttpResponse(requestProps, resp, buffer, responseFilters, OidcEndpoint.Type.TOKEN); if (resp.statusCode() == 200) { LOG.debugf("%s OidcClient has %s the tokens", oidcConfig.getId().get(), (refresh ? "refreshed" : "acquired")); - JsonObject json = resp.bodyAsJsonObject(); + JsonObject json = buffer.toJsonObject(); // access token final String accessToken = json.getString(oidcConfig.grant.accessTokenProperty); final Long accessTokenExpiresAt = getExpiresAtValue(accessToken, json.getValue(oidcConfig.grant.expiresInProperty)); @@ -223,7 +253,7 @@ private Tokens emitGrantTokens(HttpResponse resp, boolean refresh) { return new Tokens(accessToken, accessTokenExpiresAt, oidcConfig.refreshTokenTimeSkew.orElse(null), refreshToken, refreshTokenExpiresAt, json, oidcConfig.clientId.orElse(DEFAULT_OIDC_CLIENT_ID)); } else { - String errorMessage = resp.bodyAsString(); + String errorMessage = buffer.toString(); LOG.debugf("%s OidcClient has failed to complete the %s grant request: status: %d, error message: %s", oidcConfig.getId().get(), (refresh ? OidcConstants.REFRESH_TOKEN_GRANT : grantType), resp.statusCode(), errorMessage); @@ -290,12 +320,12 @@ private void checkClosed() { } } - private HttpRequest filter(OidcEndpoint.Type endpointType, HttpRequest request, Buffer body) { - if (!filters.isEmpty()) { - OidcRequestContextProperties props = new OidcRequestContextProperties( - Map.of(CLIENT_ID_ATTRIBUTE, oidcConfig.getId().orElse(DEFAULT_OIDC_CLIENT_ID))); - OidcRequestContext context = new OidcRequestContext(request, body, props); - for (OidcRequestFilter filter : OidcCommonUtils.getMatchingOidcRequestFilters(filters, endpointType)) { + private HttpRequest filterHttpRequest( + OidcRequestContextProperties requestProps, + OidcEndpoint.Type endpointType, HttpRequest request, Buffer body) { + if (!requestFilters.isEmpty()) { + OidcRequestContext context = new OidcRequestContext(request, body, requestProps); + for (OidcRequestFilter filter : OidcCommonUtils.getMatchingOidcRequestFilters(requestFilters, endpointType)) { filter.filter(context); } } diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientRecorder.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientRecorder.java index d018f14a65700..3955d89010960 100644 --- a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientRecorder.java +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientRecorder.java @@ -24,6 +24,7 @@ import io.quarkus.oidc.common.OidcEndpoint; import io.quarkus.oidc.common.OidcRequestContextProperties; import io.quarkus.oidc.common.OidcRequestFilter; +import io.quarkus.oidc.common.OidcResponseFilter; import io.quarkus.oidc.common.runtime.OidcCommonUtils; import io.quarkus.oidc.common.runtime.OidcConstants; import io.quarkus.oidc.common.runtime.OidcTlsSupport; @@ -136,7 +137,7 @@ protected static Uni createOidcClientUni(OidcClientConfig oidcConfig WebClient client = WebClient.create(mutinyVertx, options); Map> oidcRequestFilters = OidcCommonUtils.getOidcRequestFilters(); - + Map> oidcResponseFilters = OidcCommonUtils.getOidcResponseFilters(); Uni tokenUrisUni = null; if (OidcCommonUtils.isAbsoluteUrl(oidcConfig.tokenPath)) { tokenUrisUni = Uni.createFrom().item( @@ -150,7 +151,8 @@ protected static Uni createOidcClientUni(OidcClientConfig oidcConfig OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.tokenPath), OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.revokePath))); } else { - tokenUrisUni = discoverTokenUris(client, oidcRequestFilters, authServerUriString.toString(), oidcConfig, + tokenUrisUni = discoverTokenUris(client, oidcRequestFilters, oidcResponseFilters, + authServerUriString.toString(), oidcConfig, mutinyVertx); } } @@ -213,7 +215,8 @@ public OidcClient apply(OidcConfigurationMetadata metadata, Throwable t) { tokenGrantParams, commonRefreshGrantParams, oidcConfig, - oidcRequestFilters); + oidcRequestFilters, + oidcResponseFilters); } }); @@ -232,13 +235,13 @@ private static void setGrantClientParams(OidcClientConfig oidcConfig, MultiMap g private static Uni discoverTokenUris(WebClient client, Map> oidcRequestFilters, + Map> oidcResponseFilters, String authServerUrl, OidcClientConfig oidcConfig, io.vertx.mutiny.core.Vertx vertx) { final long connectionDelayInMillisecs = OidcCommonUtils.getConnectionDelayInMillis(oidcConfig); OidcRequestContextProperties contextProps = new OidcRequestContextProperties( Map.of(CLIENT_ID_ATTRIBUTE, oidcConfig.getId().orElse(DEFAULT_OIDC_CLIENT_ID))); - return OidcCommonUtils - .discoverMetadata(client, oidcRequestFilters, contextProps, authServerUrl, connectionDelayInMillisecs, vertx, - oidcConfig.useBlockingDnsLookup) + return OidcCommonUtils.discoverMetadata(client, oidcRequestFilters, contextProps, oidcResponseFilters, + authServerUrl, connectionDelayInMillisecs, vertx, oidcConfig.useBlockingDnsLookup) .onItem().transform(json -> new OidcConfigurationMetadata(json.getString("token_endpoint"), json.getString("revocation_endpoint"))); } diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcResponseFilter.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcResponseFilter.java new file mode 100644 index 0000000000000..0fc9bfa933c39 --- /dev/null +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcResponseFilter.java @@ -0,0 +1,27 @@ +package io.quarkus.oidc.common; + +import io.vertx.mutiny.core.MultiMap; +import io.vertx.mutiny.core.buffer.Buffer; + +/** + * Response filter which can be used to intercept HTTP responses from the OIDC provider. + *

+ * Filter can be restricted to a specific OIDC endpoint with a {@link OidcEndpoint} annotation. + */ +public interface OidcResponseFilter { + + /** + * OIDC response context which provides access to the HTTP response status code, headers and body. + */ + record OidcResponseContext(OidcRequestContextProperties requestProperties, + int statusCode, MultiMap responseHeaders, Buffer responseBody) { + } + + /** + * Filter OIDC responses. + * + * @param responseContext the response context which provides access to the HTTP response status code, headers and body. + * + */ + void filter(OidcResponseContext responseContext); +} diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java index b97f3861427af..982558e472cb8 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java @@ -41,9 +41,12 @@ import io.quarkus.credentials.CredentialsProvider; import io.quarkus.credentials.runtime.CredentialsProviderFinder; import io.quarkus.oidc.common.OidcEndpoint; +import io.quarkus.oidc.common.OidcEndpoint.Type; import io.quarkus.oidc.common.OidcRequestContextProperties; import io.quarkus.oidc.common.OidcRequestFilter; import io.quarkus.oidc.common.OidcRequestFilter.OidcRequestContext; +import io.quarkus.oidc.common.OidcResponseFilter; +import io.quarkus.oidc.common.OidcResponseFilter.OidcResponseContext; import io.quarkus.oidc.common.runtime.OidcClientCommonConfig.Credentials; import io.quarkus.oidc.common.runtime.OidcClientCommonConfig.Credentials.Provider; import io.quarkus.oidc.common.runtime.OidcClientCommonConfig.Credentials.Secret; @@ -494,26 +497,30 @@ public static Predicate oidcEndpointNotAvailable() { || (t instanceof OidcEndpointAccessException && ((OidcEndpointAccessException) t).getErrorStatus() == 404)); } - public static Uni discoverMetadata(WebClient client, Map> filters, - OidcRequestContextProperties contextProperties, String authServerUrl, + public static Uni discoverMetadata(WebClient client, + Map> requestFilters, + OidcRequestContextProperties contextProperties, Map> responseFilters, + String authServerUrl, long connectionDelayInMillisecs, Vertx vertx, boolean blockingDnsLookup) { final String discoveryUrl = getDiscoveryUri(authServerUrl); HttpRequest request = client.getAbs(discoveryUrl); - if (!filters.isEmpty()) { - Map newProperties = contextProperties == null ? new HashMap<>() - : new HashMap<>(contextProperties.getAll()); - newProperties.put(OidcRequestContextProperties.DISCOVERY_ENDPOINT, discoveryUrl); - OidcRequestContextProperties requestProps = new OidcRequestContextProperties(newProperties); + final OidcRequestContextProperties requestProps = requestFilters.isEmpty() ? null + : getDiscoveryRequestProps(contextProperties, discoveryUrl); + if (!requestFilters.isEmpty()) { OidcRequestContext context = new OidcRequestContext(request, null, requestProps); - for (OidcRequestFilter filter : getMatchingOidcRequestFilters(filters, OidcEndpoint.Type.DISCOVERY)) { + for (OidcRequestFilter filter : getMatchingOidcRequestFilters(requestFilters, OidcEndpoint.Type.DISCOVERY)) { filter.filter(context); } } return sendRequest(vertx, request, blockingDnsLookup).onItem().transform(resp -> { + + Buffer buffer = resp.body(); + filterHttpResponse(requestProps, resp, buffer, responseFilters, OidcEndpoint.Type.DISCOVERY); + if (resp.statusCode() == 200) { - return resp.bodyAsJsonObject(); + return buffer.toJsonObject(); } else { - String errorMessage = resp.bodyAsString(); + String errorMessage = buffer.toString(); if (errorMessage != null && !errorMessage.isEmpty()) { LOG.warnf("Discovery request %s has failed, status code: %d, error message: %s", discoveryUrl, resp.statusCode(), errorMessage); @@ -533,6 +540,25 @@ public static Uni discoverMetadata(WebClient client, Map newProperties = contextProperties == null ? new HashMap<>() + : new HashMap<>(contextProperties.getAll()); + newProperties.put(OidcRequestContextProperties.DISCOVERY_ENDPOINT, discoveryUrl); + return new OidcRequestContextProperties(newProperties); + } + + public static void filterHttpResponse(OidcRequestContextProperties requestProps, + HttpResponse resp, Buffer buffer, + Map> responseFilters, OidcEndpoint.Type type) { + if (!responseFilters.isEmpty()) { + OidcResponseContext context = new OidcResponseContext(requestProps, resp.statusCode(), resp.headers(), buffer); + for (OidcResponseFilter filter : getMatchingOidcResponseFilters(responseFilters, type)) { + filter.filter(context); + } + } + } + public static String getDiscoveryUri(String authServerUrl) { return authServerUrl + OidcConstants.WELL_KNOWN_CONFIGURATION; } @@ -564,18 +590,26 @@ private static byte[] doRead(InputStream is) throws IOException { } public static Map> getOidcRequestFilters() { + return getOidcFilters(OidcRequestFilter.class); + } + + public static Map> getOidcResponseFilters() { + return getOidcFilters(OidcResponseFilter.class); + } + + private static Map> getOidcFilters(Class filterClass) { ArcContainer container = Arc.container(); if (container != null) { - Map> map = new HashMap<>(); - for (OidcRequestFilter filter : container.listAll(OidcRequestFilter.class).stream().map(handle -> handle.get()) + Map> map = new HashMap<>(); + for (T filter : container.listAll(filterClass).stream().map(handle -> handle.get()) .collect(Collectors.toList())) { OidcEndpoint endpoint = ClientProxy.unwrap(filter).getClass().getAnnotation(OidcEndpoint.class); if (endpoint != null) { for (OidcEndpoint.Type type : endpoint.value()) { - map.computeIfAbsent(type, k -> new ArrayList()).add(filter); + map.computeIfAbsent(type, k -> new ArrayList()).add(filter); } } else { - map.computeIfAbsent(OidcEndpoint.Type.ALL, k -> new ArrayList()).add(filter); + map.computeIfAbsent(OidcEndpoint.Type.ALL, k -> new ArrayList()).add(filter); } } return map; @@ -585,8 +619,19 @@ public static Map> getOidcRequestFilt public static List getMatchingOidcRequestFilters(Map> filters, OidcEndpoint.Type type) { - List typeSpecific = filters.get(type); - List all = filters.get(OidcEndpoint.Type.ALL); + return getMatchingOidcFilters(filters, type); + } + + public static List getMatchingOidcResponseFilters( + Map> filters, + OidcEndpoint.Type type) { + return getMatchingOidcFilters(filters, type); + } + + private static List getMatchingOidcFilters(Map> filters, + OidcEndpoint.Type type) { + List typeSpecific = filters.get(type); + List all = filters.get(OidcEndpoint.Type.ALL); if (typeSpecific == null && all == null) { return List.of(); } @@ -595,7 +640,7 @@ public static List getMatchingOidcRequestFilters(Map combined = new ArrayList<>(typeSpecific.size() + all.size()); + List combined = new ArrayList<>(typeSpecific.size() + all.size()); combined.addAll(typeSpecific); combined.addAll(all); return combined; diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java index b49cd0766fcbb..41e2e211b3b40 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java @@ -2,7 +2,6 @@ import java.io.Closeable; import java.net.ConnectException; -import java.nio.charset.StandardCharsets; import java.security.Key; import java.util.HashMap; import java.util.List; @@ -19,6 +18,7 @@ import io.quarkus.oidc.common.OidcRequestContextProperties; import io.quarkus.oidc.common.OidcRequestFilter; import io.quarkus.oidc.common.OidcRequestFilter.OidcRequestContext; +import io.quarkus.oidc.common.OidcResponseFilter; import io.quarkus.oidc.common.runtime.OidcClientCommonConfig.Credentials.Secret.Method; import io.quarkus.oidc.common.runtime.OidcCommonUtils; import io.quarkus.oidc.common.runtime.OidcConstants; @@ -50,14 +50,16 @@ public class OidcProviderClient implements Closeable { private final String clientSecretBasicAuthScheme; private final String introspectionBasicAuthScheme; private final Key clientJwtKey; - private final Map> filters; + private final Map> requestFilters; + private final Map> responseFilters; private final boolean clientSecretQueryAuthentication; public OidcProviderClient(WebClient client, Vertx vertx, OidcConfigurationMetadata metadata, OidcTenantConfig oidcConfig, - Map> filters) { + Map> requestFilters, + Map> responseFilters) { this.client = client; this.vertx = vertx; this.metadata = metadata; @@ -65,7 +67,8 @@ public OidcProviderClient(WebClient client, this.clientSecretBasicAuthScheme = OidcCommonUtils.initClientSecretBasicAuth(oidcConfig); this.clientJwtKey = OidcCommonUtils.initClientJwtKey(oidcConfig, true); this.introspectionBasicAuthScheme = initIntrospectionBasicAuthScheme(oidcConfig); - this.filters = filters; + this.requestFilters = requestFilters; + this.responseFilters = responseFilters; this.clientSecretQueryAuthentication = oidcConfig.credentials.clientSecret.method.orElse(null) == Method.QUERY; } @@ -84,38 +87,40 @@ public OidcConfigurationMetadata getMetadata() { } public Uni getJsonWebKeySet(OidcRequestContextProperties contextProperties) { + OidcRequestContextProperties requestProps = getRequestProps(contextProperties); return OidcCommonUtils .sendRequest(vertx, - filter(OidcEndpoint.Type.JWKS, client.getAbs(metadata.getJsonWebKeySetUri()), null, contextProperties), + filterHttpRequest(requestProps, OidcEndpoint.Type.JWKS, client.getAbs(metadata.getJsonWebKeySetUri()), + null, + contextProperties), oidcConfig.useBlockingDnsLookup) .onItem() - .transform(resp -> getJsonWebKeySet(resp)); + .transform(resp -> getJsonWebKeySet(requestProps, resp)); } public Uni getUserInfo(String token) { LOG.debugf("Get UserInfo on: %s auth: %s", metadata.getUserInfoUri(), OidcConstants.BEARER_SCHEME + " " + token); + OidcRequestContextProperties requestProps = getRequestProps(null, null); return OidcCommonUtils .sendRequest(vertx, - filter(OidcEndpoint.Type.USERINFO, client.getAbs(metadata.getUserInfoUri()), null, null) + filterHttpRequest(requestProps, OidcEndpoint.Type.USERINFO, client.getAbs(metadata.getUserInfoUri()), + null, null) .putHeader(AUTHORIZATION_HEADER, OidcConstants.BEARER_SCHEME + " " + token), oidcConfig.useBlockingDnsLookup) - .onItem().transform(resp -> getUserInfo(resp)); + .onItem().transform(resp -> getUserInfo(requestProps, resp)); } public Uni introspectToken(String token) { MultiMap introspectionParams = new MultiMap(io.vertx.core.MultiMap.caseInsensitiveMultiMap()); introspectionParams.add(OidcConstants.INTROSPECTION_TOKEN, token); introspectionParams.add(OidcConstants.INTROSPECTION_TOKEN_TYPE_HINT, OidcConstants.ACCESS_TOKEN_VALUE); - return getHttpResponse(metadata.getIntrospectionUri(), introspectionParams, true) - .transform(resp -> getTokenIntrospection(resp)); + OidcRequestContextProperties requestProps = getRequestProps(null, null); + return getHttpResponse(requestProps, metadata.getIntrospectionUri(), introspectionParams, true) + .transform(resp -> getTokenIntrospection(requestProps, resp)); } - private JsonWebKeySet getJsonWebKeySet(HttpResponse resp) { - if (resp.statusCode() == 200) { - return new JsonWebKeySet(resp.bodyAsString(StandardCharsets.UTF_8.name())); - } else { - throw responseException(metadata.getJsonWebKeySetUri(), resp); - } + private JsonWebKeySet getJsonWebKeySet(OidcRequestContextProperties requestProps, HttpResponse resp) { + return new JsonWebKeySet(getString(requestProps, metadata.getJsonWebKeySetUri(), resp, OidcEndpoint.Type.JWKS)); } public OidcTenantConfig getOidcConfig() { @@ -133,19 +138,22 @@ public Uni getAuthorizationCodeTokens(String code, Stri if (oidcConfig.codeGrant.extraParams != null) { codeGrantParams.addAll(oidcConfig.codeGrant.extraParams); } - return getHttpResponse(metadata.getTokenUri(), codeGrantParams, false) - .transform(resp -> getAuthorizationCodeTokens(resp)); + OidcRequestContextProperties requestProps = getRequestProps(OidcConstants.AUTHORIZATION_CODE); + return getHttpResponse(requestProps, metadata.getTokenUri(), codeGrantParams, false) + .transform(resp -> getAuthorizationCodeTokens(requestProps, resp)); } public Uni refreshAuthorizationCodeTokens(String refreshToken) { MultiMap refreshGrantParams = new MultiMap(io.vertx.core.MultiMap.caseInsensitiveMultiMap()); refreshGrantParams.add(OidcConstants.GRANT_TYPE, OidcConstants.REFRESH_TOKEN_GRANT); refreshGrantParams.add(OidcConstants.REFRESH_TOKEN_VALUE, refreshToken); - return getHttpResponse(metadata.getTokenUri(), refreshGrantParams, false) - .transform(resp -> getAuthorizationCodeTokens(resp)); + OidcRequestContextProperties requestProps = getRequestProps(OidcConstants.REFRESH_TOKEN_GRANT); + return getHttpResponse(requestProps, metadata.getTokenUri(), refreshGrantParams, false) + .transform(resp -> getAuthorizationCodeTokens(requestProps, resp)); } - private UniOnItem> getHttpResponse(String uri, MultiMap formBody, boolean introspect) { + private UniOnItem> getHttpResponse(OidcRequestContextProperties requestProps, String uri, + MultiMap formBody, boolean introspect) { HttpRequest request = client.postAbs(uri); Buffer buffer = null; @@ -197,16 +205,16 @@ private UniOnItem> getHttpResponse(String uri, MultiMap for // Retry up to three times with a one-second delay between the retries if the connection is closed. OidcEndpoint.Type endpoint = introspect ? OidcEndpoint.Type.INTROSPECTION : OidcEndpoint.Type.TOKEN; - Uni> response = filter(endpoint, request, buffer, null).sendBuffer(buffer) + Uni> response = filterHttpRequest(requestProps, endpoint, request, buffer, null).sendBuffer(buffer) .onFailure(ConnectException.class) .retry() .atMost(oidcConfig.connectionRetryCount).onFailure().transform(t -> t.getCause()); return response.onItem(); - } - private AuthorizationCodeTokens getAuthorizationCodeTokens(HttpResponse resp) { - JsonObject json = getJsonObject(metadata.getAuthorizationUri(), resp); + private AuthorizationCodeTokens getAuthorizationCodeTokens(OidcRequestContextProperties requestProps, + HttpResponse resp) { + JsonObject json = getJsonObject(requestProps, metadata.getAuthorizationUri(), resp, OidcEndpoint.Type.TOKEN); final String idToken = json.getString(OidcConstants.ID_TOKEN_VALUE); final String accessToken = json.getString(OidcConstants.ACCESS_TOKEN_VALUE); final String refreshToken = json.getString(OidcConstants.REFRESH_TOKEN_VALUE); @@ -220,34 +228,42 @@ private AuthorizationCodeTokens getAuthorizationCodeTokens(HttpResponse return new AuthorizationCodeTokens(idToken, accessToken, refreshToken, tokenExpiresIn); } - private UserInfoResponse getUserInfo(HttpResponse resp) { - return new UserInfoResponse(resp.getHeader(CONTENT_TYPE_HEADER), getString(metadata.getUserInfoUri(), resp)); + private UserInfoResponse getUserInfo(OidcRequestContextProperties requestProps, HttpResponse resp) { + return new UserInfoResponse(resp.getHeader(CONTENT_TYPE_HEADER), + getString(requestProps, metadata.getUserInfoUri(), resp, OidcEndpoint.Type.USERINFO)); } - private TokenIntrospection getTokenIntrospection(HttpResponse resp) { - return new TokenIntrospection(getString(metadata.getIntrospectionUri(), resp)); + private TokenIntrospection getTokenIntrospection(OidcRequestContextProperties requestProps, HttpResponse resp) { + return new TokenIntrospection( + getString(requestProps, metadata.getIntrospectionUri(), resp, OidcEndpoint.Type.INTROSPECTION)); } - private static JsonObject getJsonObject(String requestUri, HttpResponse resp) { + private JsonObject getJsonObject(OidcRequestContextProperties requestProps, String requestUri, HttpResponse resp, + OidcEndpoint.Type endpoint) { + Buffer buffer = resp.body(); + OidcCommonUtils.filterHttpResponse(requestProps, resp, buffer, responseFilters, endpoint); if (resp.statusCode() == 200) { LOG.debugf("Request succeeded: %s", resp.bodyAsJsonObject()); - return resp.bodyAsJsonObject(); + return buffer.toJsonObject(); } else { - throw responseException(requestUri, resp); + throw responseException(requestUri, resp, buffer); } } - private static String getString(String requestUri, HttpResponse resp) { + private String getString(final OidcRequestContextProperties requestProps, String requestUri, HttpResponse resp, + OidcEndpoint.Type endpoint) { + Buffer buffer = resp.body(); + OidcCommonUtils.filterHttpResponse(requestProps, resp, buffer, responseFilters, endpoint); if (resp.statusCode() == 200) { LOG.debugf("Request succeeded: %s", resp.bodyAsString()); - return resp.bodyAsString(); + return buffer.toString(); } else { - throw responseException(requestUri, resp); + throw responseException(requestUri, resp, buffer); } } - private static OIDCException responseException(String requestUri, HttpResponse resp) { - String errorMessage = resp.bodyAsString(); + private static OIDCException responseException(String requestUri, HttpResponse resp, Buffer buffer) { + String errorMessage = buffer.toString(); if (errorMessage != null && !errorMessage.isEmpty()) { LOG.errorf("Request %s has failed: status: %d, error message: %s", requestUri, resp.statusCode(), errorMessage); @@ -267,22 +283,40 @@ public Key getClientJwtKey() { return clientJwtKey; } - private HttpRequest filter(OidcEndpoint.Type endpointType, HttpRequest request, Buffer body, + private HttpRequest filterHttpRequest(OidcRequestContextProperties requestProps, OidcEndpoint.Type endpointType, + HttpRequest request, Buffer body, OidcRequestContextProperties contextProperties) { - if (!filters.isEmpty()) { - Map newProperties = contextProperties == null ? new HashMap<>() - : new HashMap<>(contextProperties.getAll()); - newProperties.put(OidcUtils.TENANT_ID_ATTRIBUTE, oidcConfig.getTenantId().orElse(OidcUtils.DEFAULT_TENANT_ID)); - newProperties.put(OidcConfigurationMetadata.class.getName(), metadata); - OidcRequestContextProperties newContextProperties = new OidcRequestContextProperties(newProperties); - OidcRequestContext context = new OidcRequestContext(request, body, newContextProperties); - for (OidcRequestFilter filter : OidcCommonUtils.getMatchingOidcRequestFilters(filters, endpointType)) { + if (!requestFilters.isEmpty()) { + OidcRequestContext context = new OidcRequestContext(request, body, requestProps); + for (OidcRequestFilter filter : OidcCommonUtils.getMatchingOidcRequestFilters(requestFilters, endpointType)) { filter.filter(context); } } return request; } + private OidcRequestContextProperties getRequestProps(String grantType) { + return getRequestProps(null, grantType); + } + + private OidcRequestContextProperties getRequestProps(OidcRequestContextProperties contextProperties) { + return getRequestProps(contextProperties, null); + } + + private OidcRequestContextProperties getRequestProps(OidcRequestContextProperties contextProperties, String grantType) { + if (requestFilters.isEmpty() && responseFilters.isEmpty()) { + return null; + } + Map newProperties = contextProperties == null ? new HashMap<>() + : new HashMap<>(contextProperties.getAll()); + newProperties.put(OidcUtils.TENANT_ID_ATTRIBUTE, oidcConfig.getTenantId().orElse(OidcUtils.DEFAULT_TENANT_ID)); + newProperties.put(OidcConfigurationMetadata.class.getName(), metadata); + if (grantType != null) { + newProperties.put(OidcConstants.GRANT_TYPE, grantType); + } + return new OidcRequestContextProperties(newProperties); + } + public Vertx getVertx() { return vertx; } @@ -293,4 +327,5 @@ public WebClient getWebClient() { static record UserInfoResponse(String contentType, String data) { }; + } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java index c16155322fc1d..65518fef5942f 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java @@ -40,6 +40,7 @@ import io.quarkus.oidc.common.OidcEndpoint; import io.quarkus.oidc.common.OidcRequestContextProperties; import io.quarkus.oidc.common.OidcRequestFilter; +import io.quarkus.oidc.common.OidcResponseFilter; import io.quarkus.oidc.common.runtime.OidcCommonConfig; import io.quarkus.oidc.common.runtime.OidcCommonUtils; import io.quarkus.oidc.common.runtime.OidcTlsSupport; @@ -530,6 +531,7 @@ protected static Uni createOidcClientUni(OidcTenantConfig oi WebClient client = WebClient.create(mutinyVertx, options); Map> oidcRequestFilters = OidcCommonUtils.getOidcRequestFilters(); + Map> oidcResponseFilters = OidcCommonUtils.getOidcResponseFilters(); Uni metadataUni = null; if (!oidcConfig.discoveryEnabled.orElse(true)) { @@ -539,7 +541,8 @@ protected static Uni createOidcClientUni(OidcTenantConfig oi OidcRequestContextProperties contextProps = new OidcRequestContextProperties( Map.of(OidcUtils.TENANT_ID_ATTRIBUTE, oidcConfig.getTenantId().orElse(OidcUtils.DEFAULT_TENANT_ID))); metadataUni = OidcCommonUtils - .discoverMetadata(client, oidcRequestFilters, contextProps, authServerUriString, connectionDelayInMillisecs, + .discoverMetadata(client, oidcRequestFilters, contextProps, oidcResponseFilters, authServerUriString, + connectionDelayInMillisecs, mutinyVertx, oidcConfig.useBlockingDnsLookup) .onItem() @@ -586,7 +589,8 @@ public Uni apply(OidcConfigurationMetadata metadata, Throwab + " Use 'quarkus.oidc.user-info-path' if the discovery is disabled.")); } return Uni.createFrom() - .item(new OidcProviderClient(client, vertx, metadata, oidcConfig, oidcRequestFilters)); + .item(new OidcProviderClient(client, vertx, metadata, oidcConfig, oidcRequestFilters, + oidcResponseFilters)); } }); diff --git a/integration-tests/oidc-client-reactive/src/main/java/io/quarkus/it/keycloak/TokenEndpointResponseFilter.java b/integration-tests/oidc-client-reactive/src/main/java/io/quarkus/it/keycloak/TokenEndpointResponseFilter.java new file mode 100644 index 0000000000000..d8c07da8042e7 --- /dev/null +++ b/integration-tests/oidc-client-reactive/src/main/java/io/quarkus/it/keycloak/TokenEndpointResponseFilter.java @@ -0,0 +1,29 @@ +package io.quarkus.it.keycloak; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.jboss.logging.Logger; + +import io.quarkus.arc.Unremovable; +import io.quarkus.oidc.common.OidcEndpoint; +import io.quarkus.oidc.common.OidcEndpoint.Type; +import io.quarkus.oidc.common.OidcResponseFilter; +import io.quarkus.oidc.common.runtime.OidcConstants; + +@ApplicationScoped +@Unremovable +@OidcEndpoint(value = Type.TOKEN) +public class TokenEndpointResponseFilter implements OidcResponseFilter { + private static final Logger LOG = Logger.getLogger(TokenEndpointResponseFilter.class); + + @Override + public void filter(OidcResponseContext rc) { + String contentType = rc.responseHeaders().get("Content-Type"); + if (contentType.equals("application/json") + && rc.responseBody().toJsonObject().containsKey("refresh_token") + && "refresh_token".equals(rc.requestProperties().get(OidcConstants.GRANT_TYPE))) { + LOG.debug("Tokens have been refreshed"); + } + } + +} diff --git a/integration-tests/oidc-client-reactive/src/main/resources/application.properties b/integration-tests/oidc-client-reactive/src/main/resources/application.properties index f1280b96a5b3b..769989bc98a83 100644 --- a/integration-tests/oidc-client-reactive/src/main/resources/application.properties +++ b/integration-tests/oidc-client-reactive/src/main/resources/application.properties @@ -40,5 +40,7 @@ io.quarkus.it.keycloak.MisconfiguredClientFilter/mp-rest/url=http://localhost:80 quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientImpl".min-level=TRACE quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientImpl".level=TRACE +quarkus.log.category."io.quarkus.it.keycloak.TokenEndpointResponseFilter".min-level=TRACE +quarkus.log.category."io.quarkus.it.keycloak.TokenEndpointResponseFilter".level=TRACE quarkus.log.file.enable=true quarkus.log.file.format=%C - %s%n diff --git a/integration-tests/oidc-client-reactive/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java b/integration-tests/oidc-client-reactive/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java index 34843128d033f..ee63607bdc660 100644 --- a/integration-tests/oidc-client-reactive/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java +++ b/integration-tests/oidc-client-reactive/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java @@ -116,7 +116,8 @@ public void run() throws Throwable { "quarkus log file " + accessLogFilePath + " is missing"); int tokenAcquisitionCount = 0; - int tokenRefreshedCount = 0; + int tokenRefreshedOidcClientLogCount = 0; + int tokenRefreshResponseFilterLogCount = 0; try (BufferedReader reader = new BufferedReader( new InputStreamReader(new ByteArrayInputStream(Files.readAllBytes(accessLogFilePath)), @@ -124,10 +125,13 @@ public void run() throws Throwable { String line = null; while ((line = reader.readLine()) != null) { if (line.contains("Default OidcClient has refreshed the tokens")) { - tokenRefreshedCount++; + tokenRefreshedOidcClientLogCount++; } else if (line.contains("Default OidcClient has acquired the tokens")) { tokenAcquisitionCount++; } + if (line.contains("Tokens have been refreshed")) { + tokenRefreshResponseFilterLogCount++; + } } } @@ -135,8 +139,11 @@ public void run() throws Throwable { assertEquals(1, tokenAcquisitionCount, "Log file must contain a single OidcClientImpl token acquisition confirmation"); // only the reactive filter is refreshing the token - assertEquals(1, tokenRefreshedCount, + assertEquals(1, tokenRefreshedOidcClientLogCount, "Log file must contain a single OidcClientImpl token refresh confirmation"); + + assertEquals(1, tokenRefreshResponseFilterLogCount, + "Log file must contain a single OidcResponseFilter token refresh confirmation"); } }); } diff --git a/integration-tests/oidc-client-registration/pom.xml b/integration-tests/oidc-client-registration/pom.xml index 4bca0039beda5..1c064689bcba6 100644 --- a/integration-tests/oidc-client-registration/pom.xml +++ b/integration-tests/oidc-client-registration/pom.xml @@ -32,6 +32,11 @@ + + org.awaitility + awaitility + test + io.quarkus quarkus-oidc diff --git a/integration-tests/oidc-client-registration/src/main/java/io/quarkus/it/keycloak/ClientRegistrationRequestFilter.java b/integration-tests/oidc-client-registration/src/main/java/io/quarkus/it/keycloak/ClientRegistrationRequestFilter.java new file mode 100644 index 0000000000000..f77b540da9074 --- /dev/null +++ b/integration-tests/oidc-client-registration/src/main/java/io/quarkus/it/keycloak/ClientRegistrationRequestFilter.java @@ -0,0 +1,27 @@ +package io.quarkus.it.keycloak; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.jboss.logging.Logger; + +import io.quarkus.arc.Unremovable; +import io.quarkus.oidc.common.OidcEndpoint; +import io.quarkus.oidc.common.OidcEndpoint.Type; +import io.quarkus.oidc.common.OidcRequestFilter; +import io.vertx.core.json.JsonObject; + +@ApplicationScoped +@Unremovable +@OidcEndpoint(value = Type.CLIENT_REGISTRATION) +public class ClientRegistrationRequestFilter implements OidcRequestFilter { + private static final Logger LOG = Logger.getLogger(ClientRegistrationRequestFilter.class); + + @Override + public void filter(OidcRequestContext rc) { + JsonObject body = rc.requestBody().toJsonObject(); + if ("Default Client".equals(body.getString("client_name"))) { + LOG.debug("'Default Client' registration request"); + } + } + +} diff --git a/integration-tests/oidc-client-registration/src/main/java/io/quarkus/it/keycloak/ClientRegistrationResponseFilter.java b/integration-tests/oidc-client-registration/src/main/java/io/quarkus/it/keycloak/ClientRegistrationResponseFilter.java new file mode 100644 index 0000000000000..bf5dc72e6fa67 --- /dev/null +++ b/integration-tests/oidc-client-registration/src/main/java/io/quarkus/it/keycloak/ClientRegistrationResponseFilter.java @@ -0,0 +1,29 @@ +package io.quarkus.it.keycloak; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.jboss.logging.Logger; + +import io.quarkus.arc.Unremovable; +import io.quarkus.oidc.common.OidcEndpoint; +import io.quarkus.oidc.common.OidcEndpoint.Type; +import io.quarkus.oidc.common.OidcResponseFilter; +import io.vertx.core.json.JsonObject; + +@ApplicationScoped +@Unremovable +@OidcEndpoint(value = Type.CLIENT_REGISTRATION) +public class ClientRegistrationResponseFilter implements OidcResponseFilter { + private static final Logger LOG = Logger.getLogger(ClientRegistrationResponseFilter.class); + + @Override + public void filter(OidcResponseContext rc) { + String contentType = rc.responseHeaders().get("Content-Type"); + JsonObject body = rc.responseBody().toJsonObject(); + if (contentType.startsWith("application/json") + && "Default Client".equals(body.getString("client_name"))) { + LOG.debug("'Default Client' has been registered"); + } + } + +} diff --git a/integration-tests/oidc-client-registration/src/main/java/io/quarkus/it/keycloak/RegisteredClientResponseFilter.java b/integration-tests/oidc-client-registration/src/main/java/io/quarkus/it/keycloak/RegisteredClientResponseFilter.java new file mode 100644 index 0000000000000..b1c27302adf43 --- /dev/null +++ b/integration-tests/oidc-client-registration/src/main/java/io/quarkus/it/keycloak/RegisteredClientResponseFilter.java @@ -0,0 +1,27 @@ +package io.quarkus.it.keycloak; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.jboss.logging.Logger; + +import io.quarkus.arc.Unremovable; +import io.quarkus.oidc.common.OidcEndpoint; +import io.quarkus.oidc.common.OidcEndpoint.Type; +import io.quarkus.oidc.common.OidcResponseFilter; + +@ApplicationScoped +@Unremovable +@OidcEndpoint(value = Type.REGISTERED_CLIENT) +public class RegisteredClientResponseFilter implements OidcResponseFilter { + private static final Logger LOG = Logger.getLogger(RegisteredClientResponseFilter.class); + + @Override + public void filter(OidcResponseContext rc) { + String contentType = rc.responseHeaders().get("Content-Type"); + if (contentType.startsWith("application/json") + && "Default Client Updated".equals(rc.responseBody().toJsonObject().getString("client_name"))) { + LOG.debug("Registered 'Default Client' has had its name updated to 'Default Client Updated'"); + } + } + +} diff --git a/integration-tests/oidc-client-registration/src/main/resources/application.properties b/integration-tests/oidc-client-registration/src/main/resources/application.properties index 3375eb5d00136..3f91343f19af7 100644 --- a/integration-tests/oidc-client-registration/src/main/resources/application.properties +++ b/integration-tests/oidc-client-registration/src/main/resources/application.properties @@ -15,5 +15,12 @@ quarkus.oidc-client-registration.tenant-client.metadata.client-name=Tenant Clien quarkus.oidc-client-registration.tenant-client.metadata.redirect-uri=http://localhost:8081/protected/tenant quarkus.log.category."org.htmlunit".level=ERROR -quarkus.log.category."io.quarkus.oidc.runtime".min-level=TRACE -quarkus.log.category."io.quarkus.oidc.runtime".level=TRACE + +quarkus.log.category."io.quarkus.it.keycloak.ClientRegistrationRequestFilter".min-level=TRACE +quarkus.log.category."io.quarkus.it.keycloak.ClientRegistrationRequestFilter".level=TRACE +quarkus.log.category."io.quarkus.it.keycloak.ClientRegistrationResponseFilter".min-level=TRACE +quarkus.log.category."io.quarkus.it.keycloak.ClientRegistrationResponseFilter".level=TRACE +quarkus.log.category."io.quarkus.it.keycloak.RegisteredClientResponseFilter".min-level=TRACE +quarkus.log.category."io.quarkus.it.keycloak.RegisteredClientResponseFilter".level=TRACE +quarkus.log.file.enable=true +quarkus.log.file.format=%C - %s%n diff --git a/integration-tests/oidc-client-registration/src/test/java/io/quarkus/it/keycloak/OidcClientRegistrationTest.java b/integration-tests/oidc-client-registration/src/test/java/io/quarkus/it/keycloak/OidcClientRegistrationTest.java index f752f5a5105d9..197aace36bd85 100644 --- a/integration-tests/oidc-client-registration/src/test/java/io/quarkus/it/keycloak/OidcClientRegistrationTest.java +++ b/integration-tests/oidc-client-registration/src/test/java/io/quarkus/it/keycloak/OidcClientRegistrationTest.java @@ -1,14 +1,26 @@ package io.quarkus.it.keycloak; +import static org.awaitility.Awaitility.given; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; import java.io.IOException; - +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.TimeUnit; + +import org.awaitility.core.ThrowingRunnable; import org.htmlunit.SilentCssErrorHandler; import org.htmlunit.TextPage; import org.htmlunit.WebClient; import org.htmlunit.html.HtmlForm; import org.htmlunit.html.HtmlPage; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import io.quarkus.test.junit.QuarkusTest; @@ -32,6 +44,7 @@ public void testDefaultRegisteredClientOnStartup() throws IOException { assertEquals("registered-client:Default Client Updated:alice", textPage.getContent()); } + checkLog(); } @Test @@ -130,4 +143,52 @@ private WebClient createWebClient() { return webClient; } + private void checkLog() { + final Path logDirectory = Paths.get(".", "target"); + given().await().pollInterval(100, TimeUnit.MILLISECONDS) + .atMost(10, TimeUnit.SECONDS) + .untilAsserted(new ThrowingRunnable() { + @Override + public void run() throws Throwable { + Path accessLogFilePath = logDirectory.resolve("quarkus.log"); + boolean fileExists = Files.exists(accessLogFilePath); + if (!fileExists) { + accessLogFilePath = logDirectory.resolve("target/quarkus.log"); + fileExists = Files.exists(accessLogFilePath); + } + Assertions.assertTrue(Files.exists(accessLogFilePath), + "quarkus log file " + accessLogFilePath + " is missing"); + + boolean clientRegistrationRequest = false; + boolean clientRegistered = false; + boolean registeredClientUpdated = false; + + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(new ByteArrayInputStream(Files.readAllBytes(accessLogFilePath)), + StandardCharsets.UTF_8))) { + String line = null; + while ((line = reader.readLine()) != null) { + if (line.contains("'Default Client' registration request")) { + clientRegistrationRequest = true; + } else if (line.contains("'Default Client' has been registered")) { + clientRegistered = true; + } else if (line.contains( + "Registered 'Default Client' has had its name updated to 'Default Client Updated'")) { + registeredClientUpdated = true; + } + if (clientRegistrationRequest && clientRegistered && registeredClientUpdated) { + break; + } + + } + } + assertTrue(clientRegistrationRequest, + "Log file must contain a default client registration request confirmation"); + assertTrue(clientRegistered, + "Log file must contain a default client registration confirmation"); + assertTrue(registeredClientUpdated, + "Log file must contain a a default client's name update confirmation"); + } + }); + } } diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/SignedUserInfoResponseFilter.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/SignedUserInfoResponseFilter.java new file mode 100644 index 0000000000000..b6bd4888d90be --- /dev/null +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/SignedUserInfoResponseFilter.java @@ -0,0 +1,26 @@ +package io.quarkus.it.keycloak; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.jboss.logging.Logger; + +import io.quarkus.arc.Unremovable; +import io.quarkus.oidc.common.OidcEndpoint; +import io.quarkus.oidc.common.OidcEndpoint.Type; +import io.quarkus.oidc.common.OidcResponseFilter; + +@ApplicationScoped +@Unremovable +@OidcEndpoint(value = Type.USERINFO) +public class SignedUserInfoResponseFilter implements OidcResponseFilter { + private static final Logger LOG = Logger.getLogger(SignedUserInfoResponseFilter.class); + + @Override + public void filter(OidcResponseContext rc) { + String contentType = rc.responseHeaders().get("Content-Type"); + if (contentType.startsWith("application/jwt") && rc.responseBody().toString().startsWith("ey")) { + LOG.debug("Response contains signed UserInfo"); + } + } + +} diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/TokenResponseFilter.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/TokenResponseFilter.java new file mode 100644 index 0000000000000..0a4dcb731fc7d --- /dev/null +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/TokenResponseFilter.java @@ -0,0 +1,31 @@ +package io.quarkus.it.keycloak; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.jboss.logging.Logger; + +import io.quarkus.arc.Unremovable; +import io.quarkus.oidc.common.OidcEndpoint; +import io.quarkus.oidc.common.OidcEndpoint.Type; +import io.quarkus.oidc.common.OidcResponseFilter; +import io.quarkus.oidc.common.runtime.OidcConstants; +import io.quarkus.oidc.runtime.OidcUtils; + +@ApplicationScoped +@Unremovable +@OidcEndpoint(value = Type.TOKEN) +public class TokenResponseFilter implements OidcResponseFilter { + private static final Logger LOG = Logger.getLogger(TokenResponseFilter.class); + + @Override + public void filter(OidcResponseContext rc) { + if (rc.statusCode() == 200 + && rc.responseHeaders().get("Content-Type").equals("application/json") + && OidcConstants.AUTHORIZATION_CODE.equals(rc.requestProperties().get(OidcConstants.GRANT_TYPE)) + && "code-flow-user-info-github-cached-in-idtoken" + .equals(rc.requestProperties().get(OidcUtils.TENANT_ID_ATTRIBUTE))) { + LOG.debug("Authorization code completed for tenant 'code-flow-user-info-github-cached-in-idtoken'"); + } + } + +} diff --git a/integration-tests/oidc-wiremock/src/main/resources/application.properties b/integration-tests/oidc-wiremock/src/main/resources/application.properties index 5495b0fff618d..36c0bd85cdb4e 100644 --- a/integration-tests/oidc-wiremock/src/main/resources/application.properties +++ b/integration-tests/oidc-wiremock/src/main/resources/application.properties @@ -233,6 +233,10 @@ quarkus.log.category."io.quarkus.oidc.runtime.OidcProvider".min-level=TRACE quarkus.log.category."io.quarkus.oidc.runtime.OidcProvider".level=TRACE quarkus.log.category."io.quarkus.oidc.runtime.OidcProviderClient".min-level=TRACE quarkus.log.category."io.quarkus.oidc.runtime.OidcProviderClient".level=TRACE +quarkus.log.category."io.quarkus.it.keycloak.SignedUserInfoResponseFilter".min-level=TRACE +quarkus.log.category."io.quarkus.it.keycloak.SignedUserInfoResponseFilter".level=TRACE +quarkus.log.category."io.quarkus.it.keycloak.TokenResponseFilter".min-level=TRACE +quarkus.log.category."io.quarkus.it.keycloak.TokenResponseFilter".level=TRACE quarkus.log.file.enable=true quarkus.log.file.format=%C - %s%n @@ -249,4 +253,4 @@ quarkus.native.additional-build-args=-H:IncludeResources=private.*\\.*,-H:Includ quarkus.grpc.clients.hello.host=localhost quarkus.grpc.clients.hello.port=8081 -quarkus.grpc.server.use-separate-server=false \ No newline at end of file +quarkus.grpc.server.use-separate-server=false diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java index 99e3ed8687730..f3f333faca157 100644 --- a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java @@ -423,21 +423,37 @@ public void run() throws Throwable { Assertions.assertTrue(Files.exists(accessLogFilePath), "quarkus log file " + accessLogFilePath + " is missing"); - boolean signedUserInfoLogDetected = false; + boolean lineConfirmingVerificationDetected = false; + boolean signedUserInfoResponseFilterMessageDetected = false; + boolean codeFlowCompletedResponseFilterMessageDetected = false; try (BufferedReader reader = new BufferedReader( new InputStreamReader(new ByteArrayInputStream(Files.readAllBytes(accessLogFilePath)), StandardCharsets.UTF_8))) { - String line = null; + String line; while ((line = reader.readLine()) != null) { if (line.contains("Verifying the signed UserInfo with the local JWK keys: ey")) { - signedUserInfoLogDetected = true; + lineConfirmingVerificationDetected = true; + } else if (line.contains("Response contains signed UserInfo")) { + signedUserInfoResponseFilterMessageDetected = true; + } else if (line.contains( + "Authorization code completed for tenant 'code-flow-user-info-github-cached-in-idtoken'")) { + codeFlowCompletedResponseFilterMessageDetected = true; + } + if (lineConfirmingVerificationDetected + && signedUserInfoResponseFilterMessageDetected + && codeFlowCompletedResponseFilterMessageDetected) { break; } } + } - assertTrue(signedUserInfoLogDetected, + assertTrue(lineConfirmingVerificationDetected, + "Log file must contain a record confirming that signed UserInfo is verified"); + assertTrue(signedUserInfoResponseFilterMessageDetected, "Log file must contain a record confirming that signed UserInfo is returned"); + assertTrue(codeFlowCompletedResponseFilterMessageDetected, + "Log file must contain a record confirming that the code flow is completed"); } });