From e795a35eaf1846c073973ed4ad3dbee2e19a728a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Feb 2025 22:35:00 +0000 Subject: [PATCH 01/16] Bump org.mariadb.jdbc:mariadb-java-client from 3.4.1 to 3.5.2 Bumps [org.mariadb.jdbc:mariadb-java-client](https://github.com/mariadb-corporation/mariadb-connector-j) from 3.4.1 to 3.5.2. - [Release notes](https://github.com/mariadb-corporation/mariadb-connector-j/releases) - [Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/blob/master/CHANGELOG.md) - [Commits](https://github.com/mariadb-corporation/mariadb-connector-j/compare/3.4.1...3.5.2) --- updated-dependencies: - dependency-name: org.mariadb.jdbc:mariadb-java-client dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] (cherry picked from commit 469d22c8e94040311bb9cdbb158464596e8a2a9c) --- bom/application/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 776118a7110a2..b59d553ce788b 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -119,7 +119,7 @@ 2.3.230 42.7.5 - 3.4.1 + 3.5.2 8.3.0 12.8.1.jre11 1.6.7 From ab5754cd6491490a2291d825e32233440d1e219b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoann=20Rodi=C3=A8re?= Date: Wed, 12 Feb 2025 08:22:28 +0100 Subject: [PATCH 02/16] Fix native compilation for MariaDB java client 3.5 1. Authentication plugins now have factories, so the substitution for PAM had to change. See https://github.com/mariadb-corporation/mariadb-connector-j/commit/734e7de199b6acad5ac0fdd994284d0f78526393#diff-dc08764181b86cf19de46936c85631d8c3501d4fe3b198b610bc152505960327L40-L41 2. Configuration parsing now reflectively calls methods on the builder, instead of accessing fields directly, so we must enable method reflection. It would seem field reflection is still necessary for other reasons. See https://github.com/mariadb-corporation/mariadb-connector-j/commit/a12ae9fe62891c4533143e074f73a04efcff74c6#diff-2573c2b3bd0420719971212a9d3d2268d8de6b1de6c54deb77e50f007501fe6eL949-R766 See https://github.com/mariadb-corporation/mariadb-connector-j/compare/3.5.1...3.5.2#diff-6986b23619644158a24e91357d3086a29899387d29069100be8edb11856b55a0L144-R147 See https://github.com/mariadb-corporation/mariadb-connector-j/commit/a12ae9fe62891c4533143e074f73a04efcff74c6#diff-2573c2b3bd0420719971212a9d3d2268d8de6b1de6c54deb77e50f007501fe6eL942 (cherry picked from commit 67a35e34847ecbd126315b975410a480f7df4f20) --- .../deployment/MariaDBJDBCReflections.java | 4 +-- ...endPamAuthPacketFactory_Substitutions.java | 17 +++++++++++ .../SendPamAuthPacket_Substitutions.java | 29 ------------------- 3 files changed, 19 insertions(+), 31 deletions(-) create mode 100644 extensions/jdbc/jdbc-mariadb/runtime/src/main/java/io/quarkus/jdbc/mariadb/runtime/graal/SendPamAuthPacketFactory_Substitutions.java delete mode 100644 extensions/jdbc/jdbc-mariadb/runtime/src/main/java/io/quarkus/jdbc/mariadb/runtime/graal/SendPamAuthPacket_Substitutions.java diff --git a/extensions/jdbc/jdbc-mariadb/deployment/src/main/java/io/quarkus/jdbc/mariadb/deployment/MariaDBJDBCReflections.java b/extensions/jdbc/jdbc-mariadb/deployment/src/main/java/io/quarkus/jdbc/mariadb/deployment/MariaDBJDBCReflections.java index fb23a27dc43f8..cb7ace4b224ee 100644 --- a/extensions/jdbc/jdbc-mariadb/deployment/src/main/java/io/quarkus/jdbc/mariadb/deployment/MariaDBJDBCReflections.java +++ b/extensions/jdbc/jdbc-mariadb/deployment/src/main/java/io/quarkus/jdbc/mariadb/deployment/MariaDBJDBCReflections.java @@ -14,9 +14,9 @@ void build(BuildProducer reflectiveClass) { reflectiveClass .produce(ReflectiveClassBuildItem.builder("org.mariadb.jdbc.Driver").build()); - //MariaDB's connection process requires reflective read to all fields of Configuration and its Builder: + //MariaDB's connection process requires reflective access to both fields and methods of Configuration and its Builder: reflectiveClass.produce( ReflectiveClassBuildItem.builder("org.mariadb.jdbc.Configuration", "org.mariadb.jdbc.Configuration$Builder") - .fields().build()); + .fields().methods().build()); } } diff --git a/extensions/jdbc/jdbc-mariadb/runtime/src/main/java/io/quarkus/jdbc/mariadb/runtime/graal/SendPamAuthPacketFactory_Substitutions.java b/extensions/jdbc/jdbc-mariadb/runtime/src/main/java/io/quarkus/jdbc/mariadb/runtime/graal/SendPamAuthPacketFactory_Substitutions.java new file mode 100644 index 0000000000000..01c32227bd873 --- /dev/null +++ b/extensions/jdbc/jdbc-mariadb/runtime/src/main/java/io/quarkus/jdbc/mariadb/runtime/graal/SendPamAuthPacketFactory_Substitutions.java @@ -0,0 +1,17 @@ +package io.quarkus.jdbc.mariadb.runtime.graal; + +import org.mariadb.jdbc.Configuration; +import org.mariadb.jdbc.HostAddress; + +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; + +@TargetClass(className = "org.mariadb.jdbc.plugin.authentication.standard.SendPamAuthPacketFactory") +public final class SendPamAuthPacketFactory_Substitutions { + + @Substitute + public void initialize(String authenticationData, byte[] seed, Configuration conf, HostAddress hostAddress) { + throw new UnsupportedOperationException("Authentication strategy 'dialog' is not supported in GraalVM"); + } + +} diff --git a/extensions/jdbc/jdbc-mariadb/runtime/src/main/java/io/quarkus/jdbc/mariadb/runtime/graal/SendPamAuthPacket_Substitutions.java b/extensions/jdbc/jdbc-mariadb/runtime/src/main/java/io/quarkus/jdbc/mariadb/runtime/graal/SendPamAuthPacket_Substitutions.java deleted file mode 100644 index b03ac4cb81651..0000000000000 --- a/extensions/jdbc/jdbc-mariadb/runtime/src/main/java/io/quarkus/jdbc/mariadb/runtime/graal/SendPamAuthPacket_Substitutions.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.quarkus.jdbc.mariadb.runtime.graal; - -import java.io.IOException; -import java.sql.SQLException; - -import org.mariadb.jdbc.Configuration; -import org.mariadb.jdbc.HostAddress; -import org.mariadb.jdbc.client.Context; -import org.mariadb.jdbc.client.ReadableByteBuf; -import org.mariadb.jdbc.client.socket.Reader; -import org.mariadb.jdbc.client.socket.Writer; - -import com.oracle.svm.core.annotate.Substitute; -import com.oracle.svm.core.annotate.TargetClass; - -@TargetClass(className = "org.mariadb.jdbc.plugin.authentication.standard.SendPamAuthPacket") -public final class SendPamAuthPacket_Substitutions { - - @Substitute - public void initialize(String authenticationData, byte[] seed, Configuration conf, HostAddress hostAddress) { - throw new UnsupportedOperationException("Authentication strategy 'dialog' is not supported in GraalVM"); - } - - @Substitute - public ReadableByteBuf process(Writer out, Reader in, Context context) - throws SQLException, IOException { - throw new UnsupportedOperationException("Authentication strategy 'dialog' is not supported in GraalVM"); - } -} From 852e712bf9ad3e9ace526fc19c2feadd56436692 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Wed, 12 Feb 2025 15:31:03 +0100 Subject: [PATCH 03/16] WebSockets Next: make it possible to configure max frame size - add quarkus.websockets-next.server.max-frame-size and quarkus.websockets-next.client.max-frame-size config properties - related to https://github.com/quarkusio/quarkus/issues/43381#issuecomment-2653470445 (cherry picked from commit 1f82857e44e6f28b047951a4eeec0e015269b636) --- .../test/maxframesize/MaxFrameSizeTest.java | 66 +++++++++++++++++++ .../websockets/next/test/utils/WSClient.java | 4 ++ .../next/runtime/WebSocketConnectorBase.java | 3 + .../WebSocketHttpServerOptionsCustomizer.java | 3 + .../config/WebSocketsClientRuntimeConfig.java | 6 ++ .../config/WebSocketsServerRuntimeConfig.java | 6 ++ 6 files changed, 88 insertions(+) create mode 100644 extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/maxframesize/MaxFrameSizeTest.java diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/maxframesize/MaxFrameSizeTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/maxframesize/MaxFrameSizeTest.java new file mode 100644 index 0000000000000..07b36da147bfc --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/maxframesize/MaxFrameSizeTest.java @@ -0,0 +1,66 @@ +package io.quarkus.websockets.next.test.maxframesize; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.netty.handler.codec.http.websocketx.CorruptedWebSocketFrameException; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.websockets.next.OnError; +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.quarkus.websockets.next.test.utils.WSClient; +import io.vertx.core.Vertx; +import io.vertx.core.http.WebSocketFrame; + +public class MaxFrameSizeTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(Echo.class, WSClient.class); + }) + .overrideConfigKey("quarkus.websockets-next.server.max-frame-size", "10"); + + @Inject + Vertx vertx; + + @TestHTTPResource("/echo") + URI echoUri; + + @Test + void testMaxFrameSize() throws InterruptedException, ExecutionException, TimeoutException { + WSClient client = WSClient.create(vertx).connect(echoUri); + client.socket().writeFrame(WebSocketFrame.textFrame("foo".repeat(10), false)); + assertTrue(Echo.CORRUPTED_LATCH.await(5, TimeUnit.SECONDS)); + } + + @WebSocket(path = "/echo") + public static class Echo { + + static final CountDownLatch CORRUPTED_LATCH = new CountDownLatch(1); + + @OnTextMessage + String process(String message) { + return message; + } + + @OnError + void onError(CorruptedWebSocketFrameException e) { + // Note that connection is automatically closed when CorruptedWebSocketFrameException is thrown + CORRUPTED_LATCH.countDown(); + } + + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/utils/WSClient.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/utils/WSClient.java index 926c0d1b82d15..49f123a443e22 100644 --- a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/utils/WSClient.java +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/utils/WSClient.java @@ -164,6 +164,10 @@ public void close() { disconnect(); } + public WebSocket socket() { + return socket.get(); + } + public enum ReceiverMode { BINARY, TEXT, diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectorBase.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectorBase.java index cd37858a15a11..f8f3f27895713 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectorBase.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectorBase.java @@ -153,6 +153,9 @@ protected WebSocketClientOptions populateClientOptions() { if (config.maxMessageSize().isPresent()) { clientOptions.setMaxMessageSize(config.maxMessageSize().getAsInt()); } + if (config.maxFrameSize().isPresent()) { + clientOptions.setMaxFrameSize(config.maxFrameSize().getAsInt()); + } Optional maybeTlsConfiguration = TlsConfiguration.from(tlsConfigurationRegistry, config.tlsConfigurationName()); diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketHttpServerOptionsCustomizer.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketHttpServerOptionsCustomizer.java index c2d2a89b6b626..228a7051f7d19 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketHttpServerOptionsCustomizer.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketHttpServerOptionsCustomizer.java @@ -34,6 +34,9 @@ private void customize(HttpServerOptions options) { if (config.maxMessageSize().isPresent()) { options.setMaxWebSocketMessageSize(config.maxMessageSize().getAsInt()); } + if (config.maxFrameSize().isPresent()) { + options.setMaxWebSocketFrameSize(config.maxFrameSize().getAsInt()); + } } } diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/config/WebSocketsClientRuntimeConfig.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/config/WebSocketsClientRuntimeConfig.java index 2b5614984ec47..9ee48f5914656 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/config/WebSocketsClientRuntimeConfig.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/config/WebSocketsClientRuntimeConfig.java @@ -34,6 +34,12 @@ public interface WebSocketsClientRuntimeConfig { */ OptionalInt maxMessageSize(); + /** + * The maximum size of a frame in bytes. The default values is + * {@value io.vertx.core.http.HttpClientOptions#DEFAULT_MAX_WEBSOCKET_FRAME_SIZEX}. + */ + OptionalInt maxFrameSize(); + /** * The interval after which, when set, the client sends a ping message to a connected server automatically. *

diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/config/WebSocketsServerRuntimeConfig.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/config/WebSocketsServerRuntimeConfig.java index 62f4edcce37ed..5284d7d022449 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/config/WebSocketsServerRuntimeConfig.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/config/WebSocketsServerRuntimeConfig.java @@ -40,6 +40,12 @@ public interface WebSocketsServerRuntimeConfig { */ OptionalInt maxMessageSize(); + /** + * The maximum size of a frame in bytes. The default values is + * {@value io.vertx.core.http.HttpServerOptions#DEFAULT_MAX_WEBSOCKET_FRAME_SIZE}. + */ + OptionalInt maxFrameSize(); + /** * The interval after which, when set, the server sends a ping message to a connected client automatically. *

From 8588d0d767f7bdb2e08d91f79febb2fe77605439 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Tue, 11 Feb 2025 23:00:27 +0000 Subject: [PATCH 04/16] Add OIDC client access token expires in skew (cherry picked from commit 634819ae847861779531934cc3af6f54b7916b58) --- .../quarkus/oidc/client/OidcClientConfig.java | 11 +++++++++++ .../oidc/client/OidcClientConfigBuilder.java | 18 ++++++++++++++++++ .../oidc/client/runtime/OidcClientConfig.java | 5 +++++ .../oidc/client/runtime/OidcClientImpl.java | 3 +++ .../oidc/client/OidcClientConfigImpl.java | 7 +++++++ .../src/main/resources/application.properties | 1 + .../io/quarkus/it/keycloak/OidcClientTest.java | 2 +- 7 files changed, 46 insertions(+), 1 deletion(-) diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfig.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfig.java index e0eec17e533d6..90cd987a47d19 100644 --- a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfig.java +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfig.java @@ -26,6 +26,7 @@ public OidcClientConfig(io.quarkus.oidc.client.runtime.OidcClientConfig mapping) scopes = mapping.scopes(); refreshTokenTimeSkew = mapping.refreshTokenTimeSkew(); accessTokenExpiresIn = mapping.accessTokenExpiresIn(); + accessTokenExpirySkew = mapping.accessTokenExpirySkew(); absoluteExpiresIn = mapping.absoluteExpiresIn(); grant.addConfigMappingValues(mapping.grant()); grantOptions = mapping.grantOptions(); @@ -64,6 +65,11 @@ public OidcClientConfig(io.quarkus.oidc.client.runtime.OidcClientConfig mapping) */ public Optional accessTokenExpiresIn = Optional.empty(); + /** + * Access token expiry time skew that can be added to the calculated token expiry time. + */ + public Optional accessTokenExpirySkew = Optional.empty(); + /** * If the access token 'expires_in' property should be checked as an absolute time value * as opposed to a duration relative to the current time. @@ -97,6 +103,11 @@ public Optional accessTokenExpiresIn() { return accessTokenExpiresIn; } + @Override + public Optional accessTokenExpirySkew() { + return accessTokenExpirySkew; + } + @Override public boolean absoluteExpiresIn() { return absoluteExpiresIn; diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfigBuilder.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfigBuilder.java index 6e30ad6178773..212befb6de1ab 100644 --- a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfigBuilder.java +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfigBuilder.java @@ -26,6 +26,7 @@ private static class OidcClientConfigImpl extends OidcClientCommonConfigImpl imp private final Grant grant; private final boolean absoluteExpiresIn; private final Optional accessTokenExpiresIn; + private final Optional accessTokenExpirySkew; private final Optional refreshTokenTimeSkew; private final Optional> scopes; private final boolean clientEnabled; @@ -39,6 +40,7 @@ private OidcClientConfigImpl(OidcClientConfigBuilder builder) { this.grant = builder.grant; this.absoluteExpiresIn = builder.absoluteExpiresIn; this.accessTokenExpiresIn = builder.accessTokenExpiresIn; + this.accessTokenExpirySkew = builder.accessTokenExpirySkew; this.refreshTokenTimeSkew = builder.refreshTokenTimeSkew; this.scopes = builder.scopes.isEmpty() ? Optional.empty() : Optional.of(List.copyOf(builder.scopes)); this.clientEnabled = builder.clientEnabled; @@ -70,6 +72,11 @@ public Optional accessTokenExpiresIn() { return accessTokenExpiresIn; } + @Override + public Optional accessTokenExpirySkew() { + return accessTokenExpirySkew; + } + @Override public boolean absoluteExpiresIn() { return absoluteExpiresIn; @@ -103,6 +110,7 @@ public Map headers() { private Grant grant; private boolean absoluteExpiresIn; private Optional accessTokenExpiresIn; + private Optional accessTokenExpirySkew; private Optional refreshTokenTimeSkew; private boolean clientEnabled; private Optional id; @@ -118,6 +126,7 @@ public OidcClientConfigBuilder(OidcClientConfig config) { this.grant = config.grant(); this.absoluteExpiresIn = config.absoluteExpiresIn(); this.accessTokenExpiresIn = config.accessTokenExpiresIn(); + this.accessTokenExpirySkew = config.accessTokenExpirySkew(); this.refreshTokenTimeSkew = config.refreshTokenTimeSkew(); this.clientEnabled = config.clientEnabled(); this.id = config.id(); @@ -219,6 +228,15 @@ public OidcClientConfigBuilder accessTokenExpiresIn(Duration accessTokenExpiresI return this; } + /** + * @param accessTokenExpirySkew {@link OidcClientConfig#accessTokenExpirySkew()} + * @return this builder + */ + public OidcClientConfigBuilder accessTokenExpirySkew(Duration accessTokenExpirySkew) { + this.accessTokenExpirySkew = Optional.ofNullable(accessTokenExpirySkew); + return this; + } + /** * @param refreshTokenTimeSkew {@link OidcClientConfig#refreshTokenTimeSkew()} * @return this builder diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientConfig.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientConfig.java index 8f2ebbbc9d5df..155bdd99a3ad0 100644 --- a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientConfig.java +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientConfig.java @@ -47,6 +47,11 @@ public interface OidcClientConfig extends OidcClientCommonConfig { */ Optional accessTokenExpiresIn(); + /** + * Access token expiry time skew that can be added to the calculated token expiry time. + */ + Optional accessTokenExpirySkew(); + /** * If the access token 'expires_in' property should be checked as an absolute time value * as opposed to a duration relative to the current time. 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 04f0bc32c11cc..e5b0d164ba270 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 @@ -287,6 +287,9 @@ private Long getAccessTokenExpiresAtValue(String token, Object expiresInValue) { final long now = System.currentTimeMillis() / 1000; expiresAt = now + oidcConfig.accessTokenExpiresIn().get().toSeconds(); } + if (expiresAt != null && oidcConfig.accessTokenExpirySkew().isPresent()) { + expiresAt += oidcConfig.accessTokenExpirySkew().get().getSeconds(); + } return expiresAt; } diff --git a/extensions/oidc-client/runtime/src/test/java/io/quarkus/oidc/client/OidcClientConfigImpl.java b/extensions/oidc-client/runtime/src/test/java/io/quarkus/oidc/client/OidcClientConfigImpl.java index 895e1478a1053..9878a5b93a995 100644 --- a/extensions/oidc-client/runtime/src/test/java/io/quarkus/oidc/client/OidcClientConfigImpl.java +++ b/extensions/oidc-client/runtime/src/test/java/io/quarkus/oidc/client/OidcClientConfigImpl.java @@ -49,6 +49,7 @@ enum ConfigMappingMethods { SCOPES, REFRESH_TOKEN_TIME_SKEW, ACCESS_TOKEN_EXPIRES_IN, + ACCESS_TOKEN_EXPIRY_SKEW, ABSOLUTE_EXPIRES_IN, GRANT, GRANT_TYPE, @@ -338,6 +339,12 @@ public Optional accessTokenExpiresIn() { return Optional.empty(); } + @Override + public Optional accessTokenExpirySkew() { + invocationsRecorder.put(ConfigMappingMethods.ACCESS_TOKEN_EXPIRY_SKEW, true); + return Optional.empty(); + } + @Override public boolean absoluteExpiresIn() { invocationsRecorder.put(ConfigMappingMethods.ABSOLUTE_EXPIRES_IN, true); diff --git a/integration-tests/oidc-client-wiremock/src/main/resources/application.properties b/integration-tests/oidc-client-wiremock/src/main/resources/application.properties index 79316e6568243..c650f101e1fb6 100644 --- a/integration-tests/oidc-client-wiremock/src/main/resources/application.properties +++ b/integration-tests/oidc-client-wiremock/src/main/resources/application.properties @@ -12,6 +12,7 @@ quarkus.oidc-client.configured-expires-in.client-id=quarkus-app quarkus.oidc-client.configured-expires-in.credentials.client-secret.value=secret quarkus.oidc-client.configured-expires-in.credentials.client-secret.method=post quarkus.oidc-client.configured-expires-in.access-token-expires-in=5S +quarkus.oidc-client.configured-expires-in.access-token-expiry-skew=2S quarkus.oidc-client.jwtbearer.auth-server-url=${keycloak.url} quarkus.oidc-client.jwtbearer.discovery-enabled=false diff --git a/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java b/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java index 058836d482e33..9e776cdd2c53a 100644 --- a/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java +++ b/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java @@ -63,7 +63,7 @@ public void testGetAccessTokenWithConfiguredExpiresIn() { assertEquals("access_token_without_expires_in", data[0]); long now = System.currentTimeMillis() / 1000; - long expectedExpiresAt = now + 5; + long expectedExpiresAt = now + 7; long accessTokenExpiresAt = Long.valueOf(data[1]); assertTrue(accessTokenExpiresAt >= expectedExpiresAt && accessTokenExpiresAt <= expectedExpiresAt + 4); From dfa9cda71fda74657f2950e0856f50efd57a1b87 Mon Sep 17 00:00:00 2001 From: Luis Rubiera Date: Thu, 13 Feb 2025 10:30:12 +0100 Subject: [PATCH 05/16] fix(independent-projects): throw IllegalArgumentException on failed URL decoding Previously, URLUtils.decode threw a RuntimeException when encountering invalid percent-encoded values. Now, it throws an IllegalArgumentException, ensuring that malformed input is correctly recognized as a client error. Test coverage added for: - Invalid percent encoding (e.g., %zz, %2) - Gray-area invalid UTF-8 cases (e.g., %80) - Properly encoded values (e.g., %20, form-encoded +, Japanese characters) Fixes #46197 (cherry picked from commit ed6ec4b6d5d1726275515d9892498da5d0081df1) --- .../reactive/common/util/URLUtils.java | 4 +- .../reactive/common/util/URLUtilsTest.java | 50 +++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 independent-projects/resteasy-reactive/common/runtime/src/test/java/org/jboss/resteasy/reactive/common/util/URLUtilsTest.java diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/URLUtils.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/URLUtils.java index cc4b407b4aca5..c8376149220e1 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/URLUtils.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/URLUtils.java @@ -217,8 +217,8 @@ public static String decode(String s, Charset enc, boolean decodeSlash, boolean return s; } - private static RuntimeException failedToDecodeURL(String s, Charset enc, Throwable o) { - return new RuntimeException("Failed to decode URL " + s + " to " + enc, o); + private static IllegalArgumentException failedToDecodeURL(String s, Charset enc, Throwable o) { + return new IllegalArgumentException("Failed to decode URL " + s + " to " + enc, o); } private static byte[] expandBytes(byte[] bytes) { diff --git a/independent-projects/resteasy-reactive/common/runtime/src/test/java/org/jboss/resteasy/reactive/common/util/URLUtilsTest.java b/independent-projects/resteasy-reactive/common/runtime/src/test/java/org/jboss/resteasy/reactive/common/util/URLUtilsTest.java new file mode 100644 index 0000000000000..66e252f89e08e --- /dev/null +++ b/independent-projects/resteasy-reactive/common/runtime/src/test/java/org/jboss/resteasy/reactive/common/util/URLUtilsTest.java @@ -0,0 +1,50 @@ +package org.jboss.resteasy.reactive.common.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +class URLUtilsTest { + @Test + void decodeInvalidPercentEncoding() { + String incomplete = "invalid%2"; + String invalidHex = "invalid%zz"; + + assertThrows(IllegalArgumentException.class, + () -> URLUtils.decode(incomplete, StandardCharsets.UTF_8, true, new StringBuilder())); + assertThrows(IllegalArgumentException.class, + () -> URLUtils.decode(invalidHex, StandardCharsets.UTF_8, true, new StringBuilder())); + } + + @Test + void decodeGrayAreaInvalidUtf8() { + String invalidUtf8 = "invalid%80"; + + // This is a gray area: %80 is not valid in UTF-8 as a standalone byte, + // but Java's default decoding behavior does not throw an exception. + // Instead, it replaces it with a special character (�). + // + // To enforce strict decoding, CharsetDecoder with CodingErrorAction.REPORT + // should be used inside URLUtils.decode. + String decoded = URLUtils.decode(invalidUtf8, StandardCharsets.UTF_8, true, new StringBuilder()); + + assertEquals("invalid�", decoded); // Note: This may vary depending on the JVM. + } + + @Test + void decodeValidValues() { + String path = "test%20path"; + String formEncoded = "test+path"; + String japanese = "%E3%83%86%E3%82%B9%E3%83%88"; // テスト + + assertEquals("test path", + URLUtils.decode(path, StandardCharsets.UTF_8, true, new StringBuilder())); + assertEquals("test path", + URLUtils.decode(formEncoded, StandardCharsets.UTF_8, true, true, new StringBuilder())); + assertEquals("テスト", + URLUtils.decode(japanese, StandardCharsets.UTF_8, true, new StringBuilder())); + } +} From 89a9b4825e3376bffedad035411903bfd47d7fdc Mon Sep 17 00:00:00 2001 From: George Gastaldi Date: Thu, 13 Feb 2025 21:44:42 -0300 Subject: [PATCH 06/16] Use same `smallrye-common` dependencies across independent subprojects (cherry picked from commit 59f5d439cca9173ec388dadef449de9d55f4fb00) --- independent-projects/arc/pom.xml | 8 ++++++++ independent-projects/qute/pom.xml | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/independent-projects/arc/pom.xml b/independent-projects/arc/pom.xml index f5e488a83fe76..18c7266310cca 100644 --- a/independent-projects/arc/pom.xml +++ b/independent-projects/arc/pom.xml @@ -49,6 +49,7 @@ 3.6.1.Final 2.8.0 1.6.Final + 2.10.0 3.27.3 5.10.5 @@ -77,6 +78,13 @@ + + io.smallrye.common + smallrye-common-bom + ${version.smallrye-common} + pom + import + io.quarkus.arc diff --git a/independent-projects/qute/pom.xml b/independent-projects/qute/pom.xml index 3f0eb9dfe06dd..9cb18c25c50a5 100644 --- a/independent-projects/qute/pom.xml +++ b/independent-projects/qute/pom.xml @@ -43,6 +43,7 @@ 3.2.6 1.8.0 3.6.1.Final + 2.10.0 2.8.0 @@ -53,6 +54,13 @@ + + io.smallrye.common + smallrye-common-bom + ${version.smallrye-common} + pom + import + org.junit From caeb60f4b6db787ed3e0e35a1f61d629c9802568 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Thu, 13 Feb 2025 17:11:35 +0100 Subject: [PATCH 07/16] Qute: fix template variants for templates with dot in the name - fixes #46254 (cherry picked from commit bfc17f6af1b0720c82fd62db2587be64353d6e0b) --- .../qute/deployment/QuteProcessor.java | 34 +++++++++----- .../qute/deployment/inject/InjectionTest.java | 45 +++++++++++++++++-- 2 files changed, 64 insertions(+), 15 deletions(-) diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java index 8b533481e5002..378c88e85401b 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java @@ -2427,21 +2427,33 @@ private void reportFoundInvalidTarget(BuildProducer va } @BuildStep - TemplateVariantsBuildItem collectTemplateVariants(List templatePaths) throws IOException { + TemplateVariantsBuildItem collectTemplateVariants(List templatePaths, QuteConfig config) + throws IOException { Set allPaths = templatePaths.stream().map(TemplatePathBuildItem::getPath).collect(Collectors.toSet()); + // Variants are usually used when injecting a template, e.g. @Inject Template foo + // In this case, the suffix may not specified but the correct template may be selected based on a matching variant + // For example, the HTTP Accept header may be used to find a matching variant // item -> [item.html, item.txt] - // ItemResource/item -> -> [ItemResource/item.html, ItemResource/item.xml] + // item.1 -> [item.1.html, item.1.txt] + // item -> [item.qute.html, item.qute.txt] + // ItemResource/item -> [ItemResource/item.html, ItemResource/item.xml] Map> baseToVariants = new HashMap<>(); for (String path : allPaths) { - int idx = path.indexOf('.'); - if (idx != -1) { - String base = path.substring(0, idx); - List variants = baseToVariants.get(base); - if (variants == null) { - variants = new ArrayList<>(); - baseToVariants.put(base, variants); - } - variants.add(path); + for (String suffix : config.suffixes()) { + // Iterate over all supported suffixes and register appropriate base + // item.1.html -> item.1 + // item.html -> item + // item.qute.html -> item, item.qute + // ItemResource/item.xml -> ItemResource/item + if (path.endsWith(suffix)) { + String base = path.substring(0, path.length() - (suffix.length() + 1)); + List variants = baseToVariants.get(base); + if (variants == null) { + variants = new ArrayList<>(); + baseToVariants.put(base, variants); + } + variants.add(path); + } } } LOGGER.debugf("Template variants found: %s", baseToVariants); diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/inject/InjectionTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/inject/InjectionTest.java index 36daed32a840f..ba56ce7e7785f 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/inject/InjectionTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/inject/InjectionTest.java @@ -5,6 +5,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.List; + import jakarta.enterprise.context.Dependent; import jakarta.inject.Inject; @@ -15,6 +17,9 @@ import io.quarkus.qute.Engine; import io.quarkus.qute.Location; import io.quarkus.qute.Template; +import io.quarkus.qute.Variant; +import io.quarkus.qute.runtime.QuteRecorder.QuteContext; +import io.quarkus.qute.runtime.TemplateProducer; import io.quarkus.test.QuarkusUnitTest; public class InjectionTest { @@ -23,20 +28,33 @@ public class InjectionTest { static final QuarkusUnitTest config = new QuarkusUnitTest() .withApplicationRoot((jar) -> jar .addClasses(SimpleBean.class) - .addAsResource(new StringAsset("quarkus.qute.suffixes=txt"), "application.properties") .addAsResource(new StringAsset("{this}"), "templates/foo.txt") .addAsResource(new StringAsset("{this}"), "templates/foo.qute.html") - .addAsResource(new StringAsset("{@String foo}{this}"), "templates/bars/bar.txt")); + .addAsResource(new StringAsset("{@String foo}{this}"), "templates/bars/bar.txt") + .addAsResource(new StringAsset("Hello {name}!"), "templates/foo.1.html") + .addAsResource(new StringAsset("Hello {name}!"), "templates/foo.1.txt")); @Inject SimpleBean simpleBean; + @Inject + QuteContext quteContext; + + @Inject + TemplateProducer templateProducer; + @Test public void testInjection() { assertNotNull(simpleBean.engine); assertTrue(simpleBean.engine.locate("foo.txt").isPresent()); + // foo.qute.html takes precedence + assertTrue(simpleBean.engine.locate("foo").orElseThrow().getVariant().get().getContentType().equals(Variant.TEXT_HTML)); assertTrue(simpleBean.engine.locate("foo.html").isEmpty()); - assertEquals("bar", simpleBean.foo.render("bar")); + assertEquals("bar", + simpleBean.foo.instance() + .setVariant(Variant.forContentType(Variant.TEXT_PLAIN)) + .data("bar") + .render()); assertEquals("bar", simpleBean.foo2.render("bar")); assertEquals("bar", simpleBean.bar.render("bar")); @@ -54,6 +72,25 @@ public void testInjection() { assertEquals("UTF-8", simpleBean.bar.getVariant().get().getEncoding()); assertNotNull(simpleBean.bar.getGeneratedId()); assertEquals("foo.qute.html", simpleBean.foo2.getId()); + assertEquals(Variant.TEXT_HTML, simpleBean.foo2.getVariant().get().getContentType()); + List fooVariants = quteContext.getVariants().get("foo"); + // foo -> foo.txt, foo.qute.html + assertEquals(2, fooVariants.size()); + assertTrue(fooVariants.contains("foo.txt")); + assertTrue(fooVariants.contains("foo.qute.html")); + List fooQuteVariants = quteContext.getVariants().get("foo.qute"); + // foo.qute -> foo.qute.html + assertEquals(1, fooQuteVariants.size()); + assertTrue(fooVariants.contains("foo.qute.html")); + + assertEquals("Hello <strong>Foo</strong>!", templateProducer.getInjectableTemplate("foo.1").instance() + .setVariant(Variant.forContentType(Variant.TEXT_HTML)) + .data("name", "Foo") + .render()); + assertEquals("Hello Foo!", templateProducer.getInjectableTemplate("foo.1").instance() + .setVariant(Variant.forContentType(Variant.TEXT_PLAIN)) + .data("name", "Foo") + .render()); } @Dependent @@ -73,4 +110,4 @@ public static class SimpleBean { } -} +} \ No newline at end of file From c41964901d8a6eaab91270441419e360fe96dafb Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Fri, 14 Feb 2025 10:19:14 +0200 Subject: [PATCH 08/16] Avoid creating a timer when reconnectDelay is set to Max When dealing with SSE using a Multi in the REST Client, the reconnectDelay is set to MAX_VALUE in order to essentially turn off reconnecting. In this case we need to avoid creating a timer that will linger around forever and potentially create a OOME Fixes: #46268 (cherry picked from commit 0ca3ed80a4e7114d839e46d44912e7a4d12f9117) --- .../jboss/resteasy/reactive/client/impl/SseEventSourceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/SseEventSourceImpl.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/SseEventSourceImpl.java index fbdf2cc6ff5e5..e59d198e027c1 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/SseEventSourceImpl.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/SseEventSourceImpl.java @@ -210,7 +210,7 @@ private synchronized void close(boolean clientClosed) { timerId = -1; } // schedule a new reconnect if the client closed us - if (clientClosed) { + if (clientClosed && reconnectDelay != Integer.MAX_VALUE) { timerId = vertx.setTimer(TimeUnit.MILLISECONDS.convert(reconnectDelay, reconnectUnit), this); } } From 41339dafcc2e591e3f761003b313b9c4ded44ac4 Mon Sep 17 00:00:00 2001 From: marko-bekhta Date: Thu, 13 Feb 2025 21:51:45 +0100 Subject: [PATCH 09/16] Introduce validation modes instead of a boolean flag for BV integration in ORM as well as switch to AUTO instead of the CALLBACK to enable the DDL integration by default. (cherry picked from commit 86ee3e39a0a41e6c87e5a136d2e29568fedaf940) --- .../HibernateOrmConfigPersistenceUnit.java | 29 ++++++++++++++++ .../orm/deployment/HibernateOrmProcessor.java | 18 +++++----- .../validation/JPATestValidationResource.java | 15 +++++++++ .../JPAValidationModeAutoTestCase.java | 33 +++++++++++++++++++ .../JPAValidationModeMultipleTestCase.java | 33 +++++++++++++++++++ .../JPAValidationModeNoneTestCase.java | 33 +++++++++++++++++++ ...pplication-validation-mode-auto.properties | 7 ++++ ...cation-validation-mode-multiple.properties | 6 ++++ ...pplication-validation-mode-none.properties | 6 ++++ 9 files changed, 171 insertions(+), 9 deletions(-) create mode 100644 extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/validation/JPAValidationModeAutoTestCase.java create mode 100644 extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/validation/JPAValidationModeMultipleTestCase.java create mode 100644 extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/validation/JPAValidationModeNoneTestCase.java create mode 100644 extensions/hibernate-orm/deployment/src/test/resources/application-validation-mode-auto.properties create mode 100644 extensions/hibernate-orm/deployment/src/test/resources/application-validation-mode-multiple.properties create mode 100644 extensions/hibernate-orm/deployment/src/test/resources/application-validation-mode-none.properties diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfigPersistenceUnit.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfigPersistenceUnit.java index 97a490790d418..490bb9c01783e 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfigPersistenceUnit.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfigPersistenceUnit.java @@ -664,9 +664,38 @@ interface HibernateOrmConfigPersistenceValidation { /** * Enables the Bean Validation integration. + * + * @deprecated Use {@link #mode()} instead. */ + @Deprecated(since = "3.19", forRemoval = true) @WithDefault("true") boolean enabled(); + + /** + * Defines how the Bean Validation integration behaves. + */ + @WithDefault("auto") + Set mode(); + + enum ValidationMode { + /** + * If a Bean Validation provider is present then behaves as if both {@link ValidationMode#CALLBACK} and + * {@link ValidationMode#DDL} modes are configured. Otherwise, same as {@link ValidationMode#NONE}. + */ + AUTO, + /** + * Bean Validation will perform the lifecycle event validation. + */ + CALLBACK, + /** + * Bean Validation constraints will be considered for the DDL operations. + */ + DDL, + /** + * Bean Validation integration will be disabled. + */ + NONE + } } } diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java index e13ac3ef3d543..d8002eaea49b4 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java @@ -1103,15 +1103,15 @@ private static void producePersistenceUnitDescriptorFromConfig( p.put(JAKARTA_SHARED_CACHE_MODE, SharedCacheMode.NONE); } - // Hibernate Validator integration: we force the callback mode to have bootstrap errors reported rather than validation ignored - // if there is any issue when bootstrapping Hibernate Validator. - if (capabilities.isPresent(Capability.HIBERNATE_VALIDATOR)) { - if (persistenceUnitConfig.validation().enabled()) { - descriptor.getProperties().setProperty(AvailableSettings.JAKARTA_VALIDATION_MODE, - ValidationMode.CALLBACK.name()); - } else { - descriptor.getProperties().setProperty(AvailableSettings.JAKARTA_VALIDATION_MODE, ValidationMode.NONE.name()); - } + if (!persistenceUnitConfig.validation().enabled()) { + descriptor.getProperties().setProperty(AvailableSettings.JAKARTA_VALIDATION_MODE, ValidationMode.NONE.name()); + } else { + descriptor.getProperties().setProperty( + AvailableSettings.JAKARTA_VALIDATION_MODE, + persistenceUnitConfig.validation().mode() + .stream() + .map(Enum::name) + .collect(Collectors.joining(","))); } // Discriminator Column diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/validation/JPATestValidationResource.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/validation/JPATestValidationResource.java index 5f9c41149a286..1147b5eb2643d 100644 --- a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/validation/JPATestValidationResource.java +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/validation/JPATestValidationResource.java @@ -7,9 +7,12 @@ import jakarta.persistence.EntityManager; import jakarta.transaction.Transactional; import jakarta.validation.ConstraintViolationException; +import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; +import org.hibernate.engine.spi.SessionFactoryImplementor; + import io.quarkus.hibernate.orm.MyEntity; @Path("/validation") @@ -35,4 +38,16 @@ public String save(String name) { .collect(Collectors.joining()); } } + + @GET + public String ddl() { + return "nullable: " + em.getEntityManagerFactory() + .unwrap(SessionFactoryImplementor.class) + .getMappingMetamodel() + .getEntityDescriptor(MyEntity.class) + .getEntityMappingType() + .getAttributeMapping(0) + .getAttributeMetadata() + .isNullable(); + } } diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/validation/JPAValidationModeAutoTestCase.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/validation/JPAValidationModeAutoTestCase.java new file mode 100644 index 0000000000000..95451fc587795 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/validation/JPAValidationModeAutoTestCase.java @@ -0,0 +1,33 @@ +package io.quarkus.hibernate.orm.validation; + +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.hibernate.orm.MyEntity; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class JPAValidationModeAutoTestCase { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(MyEntity.class, JPATestValidationResource.class) + .addAsResource("application-validation-mode-auto.properties", "application.properties")); + + @Test + public void testInValidEntity() { + String entityName = "Post method should not persist an entity having a Size constraint of 50 on the name column if validation was enabled."; + RestAssured.given().body(entityName).when().post("/validation").then() + .body(is("entity name too long")); + } + + @Test + public void testDDL() { + RestAssured.when().get("/validation").then() + .body(is("nullable: false")); + } + +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/validation/JPAValidationModeMultipleTestCase.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/validation/JPAValidationModeMultipleTestCase.java new file mode 100644 index 0000000000000..4c0b868e810d8 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/validation/JPAValidationModeMultipleTestCase.java @@ -0,0 +1,33 @@ +package io.quarkus.hibernate.orm.validation; + +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.hibernate.orm.MyEntity; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class JPAValidationModeMultipleTestCase { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(MyEntity.class, JPATestValidationResource.class) + .addAsResource("application-validation-mode-multiple.properties", "application.properties")); + + @Test + public void testValidEntity() { + String entityName = "Post method should not persist an entity having a Size constraint of 50 on the name column if validation was enabled."; + RestAssured.given().body(entityName).when().post("/validation").then() + .body(is("entity name too long")); + } + + @Test + public void testDDL() { + RestAssured.when().get("/validation").then() + .body(is("nullable: false")); + } + +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/validation/JPAValidationModeNoneTestCase.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/validation/JPAValidationModeNoneTestCase.java new file mode 100644 index 0000000000000..c4bde346284fd --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/validation/JPAValidationModeNoneTestCase.java @@ -0,0 +1,33 @@ +package io.quarkus.hibernate.orm.validation; + +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.hibernate.orm.MyEntity; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class JPAValidationModeNoneTestCase { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(MyEntity.class, JPATestValidationResource.class) + .addAsResource("application-validation-mode-none.properties", "application.properties")); + + @Test + public void testValidEntity() { + String entityName = "Post method should not persist an entity having a Size constraint of 50 on the name column if validation was enabled."; + RestAssured.given().body(entityName).when().post("/validation").then() + .body(is("OK")); + } + + @Test + public void testDDL() { + RestAssured.when().get("/validation").then() + .body(is("nullable: true")); + } + +} diff --git a/extensions/hibernate-orm/deployment/src/test/resources/application-validation-mode-auto.properties b/extensions/hibernate-orm/deployment/src/test/resources/application-validation-mode-auto.properties new file mode 100644 index 0000000000000..f2e5ffb894059 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/resources/application-validation-mode-auto.properties @@ -0,0 +1,7 @@ +quarkus.datasource.db-kind=h2 +quarkus.datasource.jdbc.url=jdbc:h2:mem:test + +#quarkus.hibernate-orm.log.sql=true +# we don't set it explicitly as it is a default: +#quarkus.hibernate-orm.validation.mode=AUTO +quarkus.hibernate-orm.database.generation=drop-and-create diff --git a/extensions/hibernate-orm/deployment/src/test/resources/application-validation-mode-multiple.properties b/extensions/hibernate-orm/deployment/src/test/resources/application-validation-mode-multiple.properties new file mode 100644 index 0000000000000..a9592873b0b3f --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/resources/application-validation-mode-multiple.properties @@ -0,0 +1,6 @@ +quarkus.datasource.db-kind=h2 +quarkus.datasource.jdbc.url=jdbc:h2:mem:test + +#quarkus.hibernate-orm.log.sql=true +quarkus.hibernate-orm.validation.mode=callback,ddl +quarkus.hibernate-orm.database.generation=drop-and-create diff --git a/extensions/hibernate-orm/deployment/src/test/resources/application-validation-mode-none.properties b/extensions/hibernate-orm/deployment/src/test/resources/application-validation-mode-none.properties new file mode 100644 index 0000000000000..71af204335037 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/resources/application-validation-mode-none.properties @@ -0,0 +1,6 @@ +quarkus.datasource.db-kind=h2 +quarkus.datasource.jdbc.url=jdbc:h2:mem:test + +#quarkus.hibernate-orm.log.sql=true +quarkus.hibernate-orm.validation.mode=none +quarkus.hibernate-orm.database.generation=drop-and-create From 88a4dc0e4b20dff469ac428380f40205bd978da7 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Thu, 13 Feb 2025 17:59:32 +0100 Subject: [PATCH 10/16] Add Dev Services configuration reference in all relevant guides Slightly related to #45892 (cherry picked from commit 26a0c73776efa0ea9c41f04cb66d43863ca1e280) --- docs/src/main/asciidoc/amqp-dev-services.adoc | 5 +++++ docs/src/main/asciidoc/infinispan-dev-services.adoc | 10 +++++----- docs/src/main/asciidoc/kafka-dev-services.adoc | 5 +++++ docs/src/main/asciidoc/kubernetes-dev-services.adoc | 4 ++++ docs/src/main/asciidoc/mongodb-dev-services.adoc | 4 ++++ docs/src/main/asciidoc/pulsar-dev-services.adoc | 5 +++++ docs/src/main/asciidoc/rabbitmq-dev-services.adoc | 5 +++++ docs/src/main/asciidoc/redis-dev-services.adoc | 8 ++++---- .../asciidoc/security-openid-connect-dev-services.adoc | 4 ++++ 9 files changed, 41 insertions(+), 9 deletions(-) diff --git a/docs/src/main/asciidoc/amqp-dev-services.adoc b/docs/src/main/asciidoc/amqp-dev-services.adoc index 0c1c921b86ff6..9d8f2e5bdf3a7 100644 --- a/docs/src/main/asciidoc/amqp-dev-services.adoc +++ b/docs/src/main/asciidoc/amqp-dev-services.adoc @@ -58,3 +58,8 @@ quarkus.amqp.devservices.image-name=quay.io/artemiscloud/activemq-artemis-broker IMPORTANT: The configured image must be _compatible_ with the `activemq-artemis-broker` one. The container is launched with the `AMQ_USER`, `AMQ_PASSWORD` and `AMQ_EXTRA_ARGS` environment variables. The ports 5672 and 8161 (web console) are exposed. + +[[configuration-reference-devservices]] +== Configuration reference + +include::{generated-dir}/config/quarkus-messaging-amqp_quarkus.amqp.devservices.adoc[opts=optional, leveloffset=+1] diff --git a/docs/src/main/asciidoc/infinispan-dev-services.adoc b/docs/src/main/asciidoc/infinispan-dev-services.adoc index 419d625a38923..546a588ab868c 100644 --- a/docs/src/main/asciidoc/infinispan-dev-services.adoc +++ b/docs/src/main/asciidoc/infinispan-dev-services.adoc @@ -14,15 +14,11 @@ Quarkus supports a feature called Dev Services that allows you to create various If you have docker running and have not configured `quarkus.infinispan-client.hosts`, Quarkus will automatically start an Infinispan container when running tests or dev mode, and automatically configure the connection. -The following properties are available to customize the Infinispan Dev Services: - -include::{generated-dir}/config/quarkus-infinispan-client_quarkus.infinispan-client.devservices.adoc[opts=optional, leveloffset=+1] - When running the production version of the application, the Infinispan connection need to be configured as normal, so if you want to include a production database config in your `application.properties` and continue to use Dev Services we recommend that you use the `%prod.` profile to define your Infinispan settings. -Dev Services for Infinispan relies on Docker to start the server. +Dev Services for Infinispan relies on Docker/Podman to start the server. == Connecting to the running Infinispan Server @@ -207,3 +203,7 @@ The default service name is `infinispan`. Sharing is enabled by default in dev mode, but disabled in test mode. You can disable the sharing with `quarkus.infinispan-client.devservices.shared=false` + +== Configuration reference + +include::{generated-dir}/config/quarkus-infinispan-client_quarkus.infinispan-client.devservices.adoc[opts=optional, leveloffset=+1] diff --git a/docs/src/main/asciidoc/kafka-dev-services.adoc b/docs/src/main/asciidoc/kafka-dev-services.adoc index 961b806f901f6..683fe5f7fc3ac 100644 --- a/docs/src/main/asciidoc/kafka-dev-services.adoc +++ b/docs/src/main/asciidoc/kafka-dev-services.adoc @@ -115,3 +115,8 @@ quarkus.kafka.devservices.redpanda.transaction-enabled=false ---- NOTE: Redpanda transactions does not support exactly-once processing. + +[[configuration-reference-devservices]] +== Configuration reference + +include::{generated-dir}/config/quarkus-kafka-client_quarkus.kafka.devservices.adoc[opts=optional, leveloffset=+1] diff --git a/docs/src/main/asciidoc/kubernetes-dev-services.adoc b/docs/src/main/asciidoc/kubernetes-dev-services.adoc index 85f86d1ace39d..af2b018c15fed 100644 --- a/docs/src/main/asciidoc/kubernetes-dev-services.adoc +++ b/docs/src/main/asciidoc/kubernetes-dev-services.adoc @@ -55,3 +55,7 @@ quarkus.kubernetes-client.devservices.api-version=1.22 `api-only` only starts a Kubernetes API Server. If you need a fully-featured Kubernetes cluster that can spin up Pods, you can use `k3s` or `kind`. Note that they both requires to run in Docker privileged mode. If `api-version` is not set, the latest version for the given flavor will be used. Otherwise, the version must match a https://github.com/dajudge/kindcontainer/blob/master/k8s-versions.json[version supported by the given flavor]. + +== Configuration reference + +include::{generated-dir}/config/quarkus-kubernetes-client_quarkus.kubernetes-client.devservices.adoc[opts=optional, leveloffset=+1] diff --git a/docs/src/main/asciidoc/mongodb-dev-services.adoc b/docs/src/main/asciidoc/mongodb-dev-services.adoc index 0839ace7eb738..d47574a24bbcb 100644 --- a/docs/src/main/asciidoc/mongodb-dev-services.adoc +++ b/docs/src/main/asciidoc/mongodb-dev-services.adoc @@ -28,3 +28,7 @@ The default service name is `mongodb`. Sharing is enabled by default in dev mode, but disabled in test mode. You can disable the sharing with `quarkus.mongodb.devservices.shared=false`. + +== Configuration reference + +include::{generated-dir}/config/quarkus-mongodb-client_quarkus.mongodb.devservices.adoc[opts=optional, leveloffset=+1] diff --git a/docs/src/main/asciidoc/pulsar-dev-services.adoc b/docs/src/main/asciidoc/pulsar-dev-services.adoc index 6f45c5b4a4677..67a5fab61da30 100644 --- a/docs/src/main/asciidoc/pulsar-dev-services.adoc +++ b/docs/src/main/asciidoc/pulsar-dev-services.adoc @@ -69,3 +69,8 @@ The following example enables transaction support: quarkus.pulsar.devservices.broker-config.transaction-coordinator-enabled=true quarkus.pulsar.devservices.broker-config.system-topic-enabled=true ---- + +[[configuration-reference-devservices]] +== Configuration reference + +include::{generated-dir}/config/quarkus-messaging-pulsar_quarkus.pulsar.devservices.adoc[opts=optional, leveloffset=+1] diff --git a/docs/src/main/asciidoc/rabbitmq-dev-services.adoc b/docs/src/main/asciidoc/rabbitmq-dev-services.adoc index e2e986e8c412f..684e5f87609cf 100644 --- a/docs/src/main/asciidoc/rabbitmq-dev-services.adoc +++ b/docs/src/main/asciidoc/rabbitmq-dev-services.adoc @@ -137,3 +137,8 @@ Additionally, any additional arguments can be provided to the binding's definiti ---- quarkus.rabbitmq.devservices.bindings.a-binding.arguments.non-std-option=value ---- + +[[configuration-reference-devservices]] +== Configuration reference + +include::{generated-dir}/config/quarkus-messaging-rabbitmq_quarkus.rabbitmq.devservices.adoc[opts=optional, leveloffset=+1] diff --git a/docs/src/main/asciidoc/redis-dev-services.adoc b/docs/src/main/asciidoc/redis-dev-services.adoc index 8074a58e887b6..ae7f73b75a68c 100644 --- a/docs/src/main/asciidoc/redis-dev-services.adoc +++ b/docs/src/main/asciidoc/redis-dev-services.adoc @@ -15,10 +15,6 @@ Quarkus supports a feature called Dev Services that allows you to create various What that means practically, is that if you have docker running and have not configured `quarkus.redis.hosts`, Quarkus will automatically start a Redis container when running tests or dev mode, and automatically configure the connection. -Available properties to customize the Redis Dev Service. - -include::{generated-dir}/config/quarkus-redis-client_quarkus.redis.devservices.adoc[opts=optional, leveloffset=+1] - When running the production version of the application, the Redis connection need to be configured as normal, so if you want to include a production database config in your `application.properties` and continue to use Dev Services we recommend that you use the `%prod.` profile to define your Redis settings. @@ -41,3 +37,7 @@ The default service name is `redis`. Sharing is enabled by default in dev mode, but disabled in test mode. You can disable the sharing with `quarkus.redis.devservices.shared=false`. + +== Configuration reference + +include::{generated-dir}/config/quarkus-redis-client_quarkus.redis.devservices.adoc[opts=optional, leveloffset=+1] diff --git a/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc b/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc index ae743b9907460..97b2429f576c5 100644 --- a/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc @@ -432,6 +432,10 @@ image::dev-ui-oidc-dev-svc-login-for-custom-users.png[alt=Dev Services for OIDC Whichever user you choose, no password is required. +== Configuration reference + +include::{generated-dir}/config/quarkus-devservices-keycloak_quarkus.keycloak.adoc[opts=optional, leveloffset=+1] + == References * xref:dev-ui.adoc[Dev UI] From 61696f6fcd0429802df830bb7a19977bbb1bc929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Mathieu?= Date: Wed, 12 Feb 2025 14:00:05 +0100 Subject: [PATCH 11/16] Start the function only once in test If a test using `WithFunction` contains multiple test method, the function will be started multiple time, wich will fail the test. Using an atomic boolean to be sure that the function is only started once fixes the issue. Fixes #44661 (cherry picked from commit ffea74985586db4aee2cabf9bc3980649dab3588) --- .../function/test/HttpFunctionTestCase.java | 11 ++++++++++ .../test/CloudFunctionTestResource.java | 20 +++++++++++-------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/integration-tests/google-cloud-functions/src/test/java/io/quarkus/gcp/function/test/HttpFunctionTestCase.java b/integration-tests/google-cloud-functions/src/test/java/io/quarkus/gcp/function/test/HttpFunctionTestCase.java index df51044859bbe..a398498a716c8 100644 --- a/integration-tests/google-cloud-functions/src/test/java/io/quarkus/gcp/function/test/HttpFunctionTestCase.java +++ b/integration-tests/google-cloud-functions/src/test/java/io/quarkus/gcp/function/test/HttpFunctionTestCase.java @@ -21,4 +21,15 @@ public void test() { .statusCode(200) .body(is("Hello World!")); } + + // we do twice the test to be sure we can call multiple time the same function + @Test + public void test2() { + // test the function using RestAssured + when() + .get() + .then() + .statusCode(200) + .body(is("Hello World!")); + } } diff --git a/test-framework/google-cloud-functions/src/main/java/io/quarkus/google/cloud/functions/test/CloudFunctionTestResource.java b/test-framework/google-cloud-functions/src/main/java/io/quarkus/google/cloud/functions/test/CloudFunctionTestResource.java index fee2136989aed..5b1ddca8a8118 100644 --- a/test-framework/google-cloud-functions/src/main/java/io/quarkus/google/cloud/functions/test/CloudFunctionTestResource.java +++ b/test-framework/google-cloud-functions/src/main/java/io/quarkus/google/cloud/functions/test/CloudFunctionTestResource.java @@ -2,6 +2,7 @@ import java.util.Collections; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; import org.eclipse.microprofile.config.ConfigProvider; @@ -16,6 +17,7 @@ public class CloudFunctionTestResource implements QuarkusTestResourceConfigurabl private FunctionType functionType; private String functionName; private CloudFunctionsInvoker invoker; + private final AtomicBoolean started = new AtomicBoolean(false); @Override public void init(WithFunction withFunction) { @@ -30,14 +32,16 @@ public Map start() { @Override public void inject(TestInjector testInjector) { - // This is a hack, we cannot start the invoker in the start() method as Quarkus is not yet initialized, - // so we start it here as this method is called later (the same for reading the test port). - int port = ConfigProvider.getConfig().getOptionalValue("quarkus.http.test-port", Integer.class).orElse(8081); - this.invoker = new CloudFunctionsInvoker(functionType, port); - try { - this.invoker.start(); - } catch (Exception e) { - throw new RuntimeException(e); + if (started.compareAndSet(false, true)) { + // This is a hack, we cannot start the invoker in the start() method as Quarkus is not yet initialized, + // so we start it here as this method is called later (the same for reading the test port). + int port = ConfigProvider.getConfig().getOptionalValue("quarkus.http.test-port", Integer.class).orElse(8081); + this.invoker = new CloudFunctionsInvoker(functionType, port); + try { + this.invoker.start(); + } catch (Exception e) { + throw new RuntimeException(e); + } } } From 3f4a2f5ab128a3c58fe34d7e75798d3287b0c704 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Fri, 24 Jan 2025 19:13:57 +0000 Subject: [PATCH 12/16] Support for OAuth2 Demonstrating Proof of Possession (cherry picked from commit 802f60caf1a246c27d3f33366e8f18fe4697842a) --- ...rity-oidc-bearer-token-authentication.adoc | 25 +- .../keycloak/KeycloakDevServicesConfig.java | 9 + .../KeycloakDevServicesProcessor.java | 15 +- .../oidc/common/runtime/OidcCommonUtils.java | 4 + .../oidc/common/runtime/OidcConstants.java | 9 + .../BearerAuthenticationMechanism.java | 65 + .../oidc/runtime/OidcIdentityProvider.java | 148 +- .../io/quarkus/oidc/runtime/OidcProvider.java | 4 +- .../oidc/runtime/OidcProviderClientImpl.java | 3 +- .../io/quarkus/oidc/runtime/OidcUtils.java | 16 + integration-tests/oidc-dpop/pom.xml | 139 + .../it/keycloak/FrontendExceptionMapper.java | 18 + .../quarkus/it/keycloak/FrontendResource.java | 227 ++ .../it/keycloak/ProtectedResource.java | 41 + .../src/main/resources/application.properties | 10 + .../src/main/resources/quarkus-realm.json | 2366 +++++++++++++++++ .../it/keycloak/OidcDPopInGraalITCase.java | 7 + .../io/quarkus/it/keycloak/OidcDPopTest.java | 149 ++ integration-tests/pom.xml | 1 + 19 files changed, 3226 insertions(+), 30 deletions(-) create mode 100644 integration-tests/oidc-dpop/pom.xml create mode 100644 integration-tests/oidc-dpop/src/main/java/io/quarkus/it/keycloak/FrontendExceptionMapper.java create mode 100644 integration-tests/oidc-dpop/src/main/java/io/quarkus/it/keycloak/FrontendResource.java create mode 100644 integration-tests/oidc-dpop/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java create mode 100644 integration-tests/oidc-dpop/src/main/resources/application.properties create mode 100644 integration-tests/oidc-dpop/src/main/resources/quarkus-realm.json create mode 100644 integration-tests/oidc-dpop/src/test/java/io/quarkus/it/keycloak/OidcDPopInGraalITCase.java create mode 100644 integration-tests/oidc-dpop/src/test/java/io/quarkus/it/keycloak/OidcDPopTest.java diff --git a/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc b/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc index ab1ee4e5c4024..b9f04706861c2 100644 --- a/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc +++ b/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc @@ -1309,7 +1309,30 @@ If you set `quarkus.oidc.client-id`, but your endpoint does not require remote a Quarkus `web-app` applications always require the `quarkus.oidc.client-id` property. ==== -== Mutual TLS token binding +== Sender-constraining access tokens + +=== Demonstrating Proof of Possession (DPoP) + +https://datatracker.ietf.org/doc/html/rfc9449[RFC9449] describes a Demonstrating Proof of Possession (DPoP) mechanism for cryprographically binding an access token to the current client, preventing the access token loss and replay. + +Single page application (SPA) public clients generate DPoP proof tokens and use them to acquire and submit access tokens which are cryptograhically bound to DPoP proofs. + +Enabling DPoP support in Quarkus requires a single property. + +For example: + +[source,properties] +---- +quarkus.oidc.auth-server-url=${your_oidc_provider_url} +quarkus.oidc.token.authorization-scheme=dpop <1> +---- +<1> Require that the access tokens are provided using HTTP `Authorization DPoP` scheme value. + +After accepting such tokens, Quarkus will go through the full https://datatracker.ietf.org/doc/html/rfc9449#name-checking-dpop-proofs[DPoP token verification process]. + +Support for custom DPoP nonce providers may be offered in the future. + +=== Mutual TLS token binding https://datatracker.ietf.org/doc/html/rfc8705[RFC8705] describes a mechanism for binding access tokens to Mutual TLS (mTLS) client authentication certificates. It requires that a client certificate's SHA256 thumbprint matches a JWT token or token introspection confirmation `x5t#S256` certificate thumbprint. diff --git a/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfig.java b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfig.java index 9a0087eea9e26..ed16bed5f4655 100644 --- a/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfig.java +++ b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfig.java @@ -5,6 +5,7 @@ import java.util.Map; import java.util.Optional; import java.util.OptionalInt; +import java.util.Set; import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigRoot; @@ -120,6 +121,14 @@ public interface KeycloakDevServicesConfig { */ Optional startCommand(); + /** + * Keycloak features. + * Use this property to enable one or more experimental Keycloak features. + * Note, if you also have to customize a Keycloak {@link #startCommand()}, you can use + * a `--features` option as part of the start command sequence, instead of configuring this property. + */ + Optional> features(); + /** * The name of the Keycloak realm. * diff --git a/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesProcessor.java b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesProcessor.java index f9e679260c6b1..4abc2f9592cab 100644 --- a/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesProcessor.java +++ b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesProcessor.java @@ -411,6 +411,7 @@ private static RunningDevService startContainer( capturedDevServicesConfiguration.shared(), capturedDevServicesConfiguration.javaOpts(), capturedDevServicesConfiguration.startCommand(), + capturedDevServicesConfiguration.features(), capturedDevServicesConfiguration.showLogs(), capturedDevServicesConfiguration.containerMemoryLimit(), errors); @@ -485,14 +486,15 @@ private static class QuarkusOidcContainer extends GenericContainer realmReps = new LinkedList<>(); private final Optional startCommand; + private final Optional> features; private final boolean showLogs; private final MemorySize containerMemoryLimit; private final List errors; public QuarkusOidcContainer(DockerImageName dockerImageName, OptionalInt fixedExposedPort, boolean useSharedNetwork, List realmPaths, Map resources, String containerLabelValue, - boolean sharedContainer, Optional javaOpts, Optional startCommand, boolean showLogs, - MemorySize containerMemoryLimit, List errors) { + boolean sharedContainer, Optional javaOpts, Optional startCommand, + Optional> features, boolean showLogs, MemorySize containerMemoryLimit, List errors) { super(dockerImageName); this.useSharedNetwork = useSharedNetwork; @@ -512,6 +514,7 @@ public QuarkusOidcContainer(DockerImageName dockerImageName, OptionalInt fixedEx this.fixedExposedPort = fixedExposedPort; this.startCommand = startCommand; + this.features = features; this.showLogs = showLogs; this.containerMemoryLimit = containerMemoryLimit; this.errors = errors; @@ -554,8 +557,12 @@ protected void configure() { if (keycloakX) { addEnv(KEYCLOAK_QUARKUS_ADMIN_PROP, KEYCLOAK_ADMIN_USER); addEnv(KEYCLOAK_QUARKUS_ADMIN_PASSWORD_PROP, KEYCLOAK_ADMIN_PASSWORD); - withCommand(startCommand.orElse(KEYCLOAK_QUARKUS_START_CMD) - + (useSharedNetwork ? " --hostname-backchannel-dynamic true" : "")); + String finalStartCommand = startCommand.orElse(KEYCLOAK_QUARKUS_START_CMD) + + (useSharedNetwork ? " --hostname-backchannel-dynamic true" : ""); + if (features.isPresent()) { + finalStartCommand += (" --features=" + features.get().stream().collect(Collectors.joining(","))); + } + withCommand(finalStartCommand); addUpConfigResource(); if (isHttps()) { addExposedPort(KEYCLOAK_HTTPS_PORT); 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 224ad6af5ab78..1973cae9c2419 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 @@ -782,6 +782,10 @@ public static String base64UrlDecode(String encodedContent) { return new String(Base64.getUrlDecoder().decode(encodedContent), StandardCharsets.UTF_8); } + public static String base64UrlEncode(byte[] bytes) { + return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); + } + public static JsonObject decodeAsJsonObject(String encodedContent) { try { return new JsonObject(base64UrlDecode(encodedContent)); diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcConstants.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcConstants.java index 73b6dde2e7225..04d81088945b7 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcConstants.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcConstants.java @@ -39,6 +39,8 @@ public final class OidcConstants { public static final String PASSWORD_GRANT_USERNAME = "username"; public static final String PASSWORD_GRANT_PASSWORD = "password"; + public static final String TOKEN_TYPE_HEADER = "typ"; + public static final String TOKEN_ALGORITHM_HEADER = "alg"; public static final String TOKEN_SCOPE = "scope"; public static final String GRANT_TYPE = "grant_type"; @@ -46,6 +48,7 @@ public final class OidcConstants { public static final String CLIENT_SECRET = "client_secret"; public static final String BEARER_SCHEME = "Bearer"; + public static final String DPOP_SCHEME = "DPoP"; public static final String BASIC_SCHEME = "Basic"; public static final String AUTHORIZATION_CODE = "authorization_code"; @@ -89,4 +92,10 @@ public final class OidcConstants { public static final String CONFIRMATION_CLAIM = "cnf"; public static final String X509_SHA256_THUMBPRINT = "x5t#S256"; + public static final String DPOP_TOKEN_TYPE = "dpop+jwt"; + public static final String DPOP_JWK_SHA256_THUMBPRINT = "jkt"; + public static final String DPOP_JWK_HEADER = "jwk"; + public static final String DPOP_ACCESS_TOKEN_THUMBPRINT = "ath"; + public static final String DPOP_HTTP_METHOD = "htm"; + public static final String DPOP_HTTP_REQUEST_URI = "htu"; } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BearerAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BearerAuthenticationMechanism.java index 8fabfca0601ba..941fe6a1574e3 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BearerAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BearerAuthenticationMechanism.java @@ -4,6 +4,7 @@ import java.security.cert.Certificate; import java.security.cert.X509Certificate; +import java.util.List; import java.util.function.Function; import javax.net.ssl.SSLPeerUnverifiedException; @@ -14,12 +15,14 @@ import io.netty.handler.codec.http.HttpResponseStatus; import io.quarkus.oidc.AccessTokenCredential; import io.quarkus.oidc.OidcTenantConfig; +import io.quarkus.oidc.common.runtime.OidcCommonUtils; import io.quarkus.oidc.common.runtime.OidcConstants; import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.identity.IdentityProviderManager; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.vertx.http.runtime.security.ChallengeData; import io.smallrye.mutiny.Uni; +import io.vertx.core.json.JsonObject; import io.vertx.ext.web.RoutingContext; public class BearerAuthenticationMechanism extends AbstractOidcAuthenticationMechanism { @@ -33,6 +36,7 @@ public Uni authenticate(RoutingContext context, if (token != null) { try { setCertificateThumbprint(context, oidcTenantConfig); + setDPopProof(context, oidcTenantConfig); } catch (AuthenticationFailedException ex) { return Uni.createFrom().failure(ex); } @@ -54,6 +58,67 @@ private static void setCertificateThumbprint(RoutingContext context, OidcTenantC } } + private static void setDPopProof(RoutingContext context, OidcTenantConfig oidcTenantConfig) { + if (OidcConstants.DPOP_SCHEME.equals(oidcTenantConfig.token().authorizationScheme())) { + + List proofs = context.request().headers().getAll(OidcConstants.DPOP_SCHEME); + if (proofs == null || proofs.isEmpty()) { + LOG.warn("DPOP proof header must be present to verify the DPOP access token binding"); + throw new AuthenticationFailedException(); + } + if (proofs.size() != 1) { + LOG.warn("Only a single DPOP proof header is accepted"); + throw new AuthenticationFailedException(); + } + String proof = proofs.get(0); + + // Initial proof check: + JsonObject proofJwtHeaders = OidcUtils.decodeJwtHeaders(proof); + JsonObject proofJwtClaims = OidcCommonUtils.decodeJwtContent(proof); + + if (!OidcConstants.DPOP_TOKEN_TYPE.equals(proofJwtHeaders.getString(OidcConstants.TOKEN_TYPE_HEADER))) { + LOG.warn("Invalid DPOP proof token type ('typ') header"); + throw new AuthenticationFailedException(); + } + + // Check HTTP method and request URI + String proofHttpMethod = proofJwtClaims.getString(OidcConstants.DPOP_HTTP_METHOD); + if (proofHttpMethod == null) { + LOG.warn("DPOP proof HTTP method claim is missing"); + throw new AuthenticationFailedException(); + } + + String httpMethod = context.request().method().name(); + if (!httpMethod.equals(proofHttpMethod)) { + LOG.warnf("DPOP proof HTTP method claim %s does not match the request HTTP method %s", proofHttpMethod, + httpMethod); + throw new AuthenticationFailedException(); + } + + // Check HTTP request URI + String proofHttpRequestUri = proofJwtClaims.getString(OidcConstants.DPOP_HTTP_REQUEST_URI); + if (proofHttpRequestUri == null) { + LOG.warn("DPOP proof HTTP request uri claim is missing"); + throw new AuthenticationFailedException(); + } + + String httpRequestUri = context.request().absoluteURI(); + int queryIndex = httpRequestUri.indexOf("?"); + if (queryIndex > 0) { + httpRequestUri = httpRequestUri.substring(0, queryIndex); + } + if (!httpRequestUri.equals(proofHttpRequestUri)) { + LOG.warnf("DPOP proof HTTP request uri claim %s does not match the request HTTP uri %s", proofHttpRequestUri, + httpRequestUri); + throw new AuthenticationFailedException(); + } + + context.put(OidcUtils.DPOP_PROOF, proof); + context.put(OidcUtils.DPOP_PROOF_JWT_HEADERS, proofJwtHeaders); + context.put(OidcUtils.DPOP_PROOF_JWT_CLAIMS, proofJwtClaims); + } + } + private static Certificate getCertificate(RoutingContext context) { try { return context.request().sslSession().getPeerCertificates()[0]; diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java index 434ed37149822..1d214f88dea87 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java @@ -3,6 +3,7 @@ import static io.quarkus.oidc.runtime.OidcUtils.validateAndCreateIdentity; import static io.quarkus.vertx.http.runtime.security.HttpSecurityUtils.getRoutingContextAttribute; +import java.security.NoSuchAlgorithmException; import java.security.Principal; import java.util.Map; import java.util.Set; @@ -14,6 +15,9 @@ import org.eclipse.microprofile.jwt.Claims; import org.jboss.logging.Logger; +import org.jose4j.jwk.PublicJsonWebKey; +import org.jose4j.jws.JsonWebSignature; +import org.jose4j.lang.JoseException; import org.jose4j.lang.UnresolvableKeyException; import io.quarkus.oidc.AccessTokenCredential; @@ -201,33 +205,120 @@ private Uni verifyPrimaryTokenUni(Map r final boolean idToken = isIdToken(request); Uni result = verifyTokenUni(requestData, resolvedContext, request.getToken(), idToken, false, userInfo); - if (!idToken && resolvedContext.oidcConfig().token().binding().certificate()) { - return result.onItem().transform(new Function() { + if (!idToken) { + if (resolvedContext.oidcConfig().token().binding().certificate()) { + result = result.onItem().transform(new Function() { - @Override - public TokenVerificationResult apply(TokenVerificationResult t) { - String tokenCertificateThumbprint = getTokenCertThumbprint(requestData, t); - if (tokenCertificateThumbprint == null) { - LOG.warn( - "Access token does not contain a confirmation 'cnf' claim with the certificate thumbprint"); - throw new AuthenticationFailedException(); - } - String clientCertificateThumbprint = (String) requestData.get(OidcConstants.X509_SHA256_THUMBPRINT); - if (clientCertificateThumbprint == null) { - LOG.warn("Client certificate thumbprint is not available"); - throw new AuthenticationFailedException(); + @Override + public TokenVerificationResult apply(TokenVerificationResult t) { + String tokenCertificateThumbprint = getTokenCertThumbprint(requestData, t); + if (tokenCertificateThumbprint == null) { + LOG.warn( + "Access token does not contain a confirmation 'cnf' claim with the certificate thumbprint"); + throw new AuthenticationFailedException(); + } + String clientCertificateThumbprint = (String) requestData.get(OidcConstants.X509_SHA256_THUMBPRINT); + if (clientCertificateThumbprint == null) { + LOG.warn("Client certificate thumbprint is not available"); + throw new AuthenticationFailedException(); + } + if (!clientCertificateThumbprint.equals(tokenCertificateThumbprint)) { + LOG.warn("Client certificate thumbprint does not match the token certificate thumbprint"); + throw new AuthenticationFailedException(); + } + return t; } - if (!clientCertificateThumbprint.equals(tokenCertificateThumbprint)) { - LOG.warn("Client certificate thumbprint does not match the token certificate thumbprint"); - throw new AuthenticationFailedException(); + + }); + } + + if (requestData.containsKey(OidcUtils.DPOP_PROOF_JWT_HEADERS)) { + result = result.onItem().transform(new Function() { + + @Override + public TokenVerificationResult apply(TokenVerificationResult t) { + + String dpopJwkThumbprint = getDpopJwkThumbprint(requestData, t); + if (dpopJwkThumbprint == null) { + LOG.warn( + "DPoP access token does not contain a confirmation 'cnf' claim with the JWK thumbprint"); + throw new AuthenticationFailedException(); + } + + JsonObject proofHeaders = (JsonObject) requestData.get(OidcUtils.DPOP_PROOF_JWT_HEADERS); + + JsonObject jwkProof = proofHeaders.getJsonObject(OidcConstants.DPOP_JWK_HEADER); + if (jwkProof == null) { + LOG.warn("DPoP proof jwk header is missing"); + throw new AuthenticationFailedException(); + } + + PublicJsonWebKey publicJsonWebKey = null; + try { + publicJsonWebKey = PublicJsonWebKey.Factory.newPublicJwk(jwkProof.getMap()); + } catch (JoseException ex) { + LOG.warn("DPoP proof jwk header does not represent a valid JWK key"); + throw new AuthenticationFailedException(ex); + } + + if (publicJsonWebKey.getPrivateKey() != null) { + LOG.warn("DPoP proof JWK key is a private key but it must be a public key"); + throw new AuthenticationFailedException(); + } + + byte[] jwkProofDigest = publicJsonWebKey.calculateThumbprint("SHA-256"); + String jwkProofThumbprint = OidcCommonUtils.base64UrlEncode(jwkProofDigest); + + if (!dpopJwkThumbprint.equals(jwkProofThumbprint)) { + LOG.warn("DPoP access token JWK thumbprint does not match the DPoP proof JWK thumbprint"); + throw new AuthenticationFailedException(); + } + + try { + JsonWebSignature jws = new JsonWebSignature(); + jws.setAlgorithmConstraints(OidcProvider.ASYMMETRIC_ALGORITHM_CONSTRAINTS); + jws.setCompactSerialization((String) requestData.get(OidcUtils.DPOP_PROOF)); + jws.setKey(publicJsonWebKey.getPublicKey()); + if (!jws.verifySignature()) { + LOG.warn("DPoP proof token signature is invalid"); + throw new AuthenticationFailedException(); + } + } catch (JoseException ex) { + LOG.warn("DPoP proof token signature can not be verified"); + throw new AuthenticationFailedException(ex); + } + + JsonObject proofClaims = (JsonObject) requestData.get(OidcUtils.DPOP_PROOF_JWT_CLAIMS); + + // Calculate the access token thumprint and compare with the `ath` claim + + String accessTokenProof = proofClaims.getString(OidcConstants.DPOP_ACCESS_TOKEN_THUMBPRINT); + if (accessTokenProof == null) { + LOG.warn("DPoP proof access token hash is missing"); + throw new AuthenticationFailedException(); + } + + String accessTokenHash = null; + try { + accessTokenHash = OidcCommonUtils.base64UrlEncode( + OidcUtils.getSha256Digest(request.getToken().getToken())); + } catch (NoSuchAlgorithmException ex) { + // SHA256 is always supported + } + + if (!accessTokenProof.equals(accessTokenHash)) { + LOG.warn("DPoP access token hash does not match the DPoP proof access token hash"); + throw new AuthenticationFailedException(); + } + + return t; } - return t; - } - }); - } else { - return result; + }); + } } + + return result; } } @@ -243,6 +334,19 @@ private static String getTokenCertThumbprint(Map requestData, To return thumbprint; } + private static String getDpopJwkThumbprint(Map requestData, TokenVerificationResult t) { + JsonObject json = t.localVerificationResult != null ? t.localVerificationResult + : new JsonObject(t.introspectionResult.getIntrospectionString()); + JsonObject cnf = json.getJsonObject(OidcConstants.CONFIRMATION_CLAIM); + String thumbprint = cnf == null ? null : cnf.getString(OidcConstants.DPOP_JWK_SHA256_THUMBPRINT); + if (thumbprint != null) { + requestData.put( + (t.introspectionResult == null ? OidcUtils.DPOP_JWT_THUMBPRINT : OidcUtils.DPOP_INTROSPECTION_THUMBPRINT), + true); + } + return thumbprint; + } + private Uni getUserInfoAndCreateIdentity(Uni tokenUni, Map requestData, TokenAuthenticationRequest request, diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java index 64d0c9bf9343b..a1e52e97d285b 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java @@ -62,10 +62,10 @@ public class OidcProvider implements Closeable { SignatureAlgorithm.PS384.getAlgorithm(), SignatureAlgorithm.PS512.getAlgorithm(), SignatureAlgorithm.EDDSA.getAlgorithm() }; - private static final AlgorithmConstraints ASYMMETRIC_ALGORITHM_CONSTRAINTS = new AlgorithmConstraints( - AlgorithmConstraints.ConstraintType.PERMIT, ASYMMETRIC_SUPPORTED_ALGORITHMS); private static final AlgorithmConstraints SYMMETRIC_ALGORITHM_CONSTRAINTS = new AlgorithmConstraints( AlgorithmConstraints.ConstraintType.PERMIT, SignatureAlgorithm.HS256.getAlgorithm()); + static final AlgorithmConstraints ASYMMETRIC_ALGORITHM_CONSTRAINTS = new AlgorithmConstraints( + AlgorithmConstraints.ConstraintType.PERMIT, ASYMMETRIC_SUPPORTED_ALGORITHMS); static final String ANY_ISSUER = "any"; private final List customValidators; diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClientImpl.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClientImpl.java index 123853e154ef6..bfca552196afa 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClientImpl.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClientImpl.java @@ -340,7 +340,8 @@ private UniOnItem> getHttpResponse(OidcRequestContextProper } } - LOG.debugf("Get token on: %s params: %s headers: %s", metadata.getTokenUri(), formBody, request.headers()); + LOG.debugf("%s token: %s params: %s headers: %s", (introspect ? "Introspect" : "Get"), metadata.getTokenUri(), formBody, + request.headers()); // 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; diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java index 554cd51a390d9..07d0b6dfc092b 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java @@ -5,6 +5,8 @@ import static io.quarkus.oidc.common.runtime.OidcConstants.TOKEN_SCOPE; import static io.quarkus.vertx.http.runtime.security.HttpSecurityUtils.getRoutingContextAttribute; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.security.Key; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -97,6 +99,12 @@ public final class OidcUtils { public static final String STATE_COOKIE_NAME = "q_auth"; public static final String JWT_THUMBPRINT = "jwt_thumbprint"; public static final String INTROSPECTION_THUMBPRINT = "introspection_thumbprint"; + public static final String DPOP_JWT_THUMBPRINT = "dpop_jwt_thumbprint"; + public static final String DPOP_INTROSPECTION_THUMBPRINT = "dpop_introspection_thumbprint"; + public static final String DPOP_PROOF = "dpop_proof"; + public static final String DPOP_PROOF_JWT_HEADERS = "dpop_proof_jwt_headers"; + public static final String DPOP_PROOF_JWT_CLAIMS = "dpop_proof_jwt_claims"; + private static final String APPLICATION_JWT = "application/jwt"; // Browsers enforce that the total Set-Cookie expression such as @@ -570,6 +578,14 @@ static OidcTenantConfig resolveProviderConfig(OidcTenantConfig oidcTenantConfig) } + public static byte[] getSha256Digest(String value) throws NoSuchAlgorithmException { + return getSha256Digest(value, StandardCharsets.UTF_8); + } + + public static byte[] getSha256Digest(String value, Charset charset) throws NoSuchAlgorithmException { + return getSha256Digest(value.getBytes(charset)); + } + public static byte[] getSha256Digest(byte[] value) throws NoSuchAlgorithmException { MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); sha256.update(value); diff --git a/integration-tests/oidc-dpop/pom.xml b/integration-tests/oidc-dpop/pom.xml new file mode 100644 index 0000000000000..59b0f53430fd2 --- /dev/null +++ b/integration-tests/oidc-dpop/pom.xml @@ -0,0 +1,139 @@ + + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + ../ + + 4.0.0 + + quarkus-integration-test-oidc-dpop + Quarkus - Integration Tests - OpenID Connect DPoP + Module that contains OpenID Connect DPoP tests + + + + io.quarkus + quarkus-oidc + + + io.quarkus + quarkus-oidc-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-rest + + + io.quarkus + quarkus-rest-deployment + ${project.version} + pom + test + + + * + * + + + + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + org.htmlunit + htmlunit + test + + + + + + + maven-surefire-plugin + + true + + + + maven-failsafe-plugin + + true + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + + test-keycloak + + + test-containers + + + + + + maven-surefire-plugin + + false + + + + maven-failsafe-plugin + + false + + ${keycloak.url} + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + + diff --git a/integration-tests/oidc-dpop/src/main/java/io/quarkus/it/keycloak/FrontendExceptionMapper.java b/integration-tests/oidc-dpop/src/main/java/io/quarkus/it/keycloak/FrontendExceptionMapper.java new file mode 100644 index 0000000000000..9660dacdf54a3 --- /dev/null +++ b/integration-tests/oidc-dpop/src/main/java/io/quarkus/it/keycloak/FrontendExceptionMapper.java @@ -0,0 +1,18 @@ +package io.quarkus.it.keycloak; + +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +import io.quarkus.security.AuthenticationFailedException; + +@Provider +public class FrontendExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(AuthenticationFailedException t) { + return Response.ok("401 status from ProtectedResource").build(); + + } + +} diff --git a/integration-tests/oidc-dpop/src/main/java/io/quarkus/it/keycloak/FrontendResource.java b/integration-tests/oidc-dpop/src/main/java/io/quarkus/it/keycloak/FrontendResource.java new file mode 100644 index 0000000000000..9f2bd4db54adc --- /dev/null +++ b/integration-tests/oidc-dpop/src/main/java/io/quarkus/it/keycloak/FrontendResource.java @@ -0,0 +1,227 @@ +package io.quarkus.it.keycloak; + +import java.net.URI; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.util.UUID; + +import jakarta.annotation.PreDestroy; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.NewCookie; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; + +import io.quarkus.oidc.OidcConfigurationMetadata; +import io.quarkus.oidc.common.runtime.OidcCommonUtils; +import io.quarkus.oidc.common.runtime.OidcConstants; +import io.quarkus.oidc.runtime.OidcUtils; +import io.quarkus.oidc.runtime.TenantConfigBean; +import io.smallrye.jwt.build.Jwt; +import io.smallrye.jwt.build.JwtClaimsBuilder; +import io.smallrye.jwt.build.JwtSignatureBuilder; +import io.smallrye.jwt.util.KeyUtils; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.json.JsonObject; +import io.vertx.mutiny.core.MultiMap; +import io.vertx.mutiny.core.Vertx; +import io.vertx.mutiny.core.buffer.Buffer; +import io.vertx.mutiny.ext.web.client.HttpRequest; +import io.vertx.mutiny.ext.web.client.HttpResponse; +import io.vertx.mutiny.ext.web.client.WebClient; + +@Path("/single-page-app") +public class FrontendResource { + + @Inject + TenantConfigBean oidcTenants; + + @Context + UriInfo ui; + + private final WebClient client; + + @Inject + public FrontendResource(Vertx vertx) { + this.client = WebClient.create(vertx); + } + + @GET + @Path("login-jwt") + public Response loginJwt() { + return redirect("dpop-jwt", "callback-jwt"); + } + + @GET + @Path("callback-jwt") + public Response callbackJwt(@QueryParam("code") String code) throws Exception { + return callProtectedEndpoint(code, "dpop-jwt", "callback-jwt", "GET", "dpop-jwt", "dpop-jwt", false, false, false); + } + + @GET + @Path("login-jwt-wrong-dpop-http-method") + public Response loginJwtWrongDpopHttpMethod() { + return redirect("dpop-jwt", "callback-jwt-wrong-dpop-http-method"); + } + + @GET + @Path("callback-jwt-wrong-dpop-http-method") + public Response callbackWrongDpopHttpMethod(@QueryParam("code") String code) throws Exception { + return callProtectedEndpoint(code, "dpop-jwt", "callback-jwt-wrong-dpop-http-method", "POST", "dpop-jwt", "dpop-jwt", + false, false, false); + } + + @GET + @Path("login-jwt-wrong-dpop-http-uri") + public Response loginJwtWrongDpopHttpUri() { + return redirect("dpop-jwt", "callback-jwt-wrong-dpop-http-uri"); + } + + @GET + @Path("callback-jwt-wrong-dpop-http-uri") + public Response callbackWrongDpopHttpUri(@QueryParam("code") String code) throws Exception { + return callProtectedEndpoint(code, "dpop-jwt", "callback-jwt-wrong-dpop-http-uri", "GET", "dpop-jwt-wrong-uri", + "dpop-jwt", false, false, false); + } + + @GET + @Path("login-jwt-wrong-dpop-signature") + public Response loginJwtWrongDpopSignature() { + return redirect("dpop-jwt", "callback-jwt-wrong-dpop-signature"); + } + + @GET + @Path("callback-jwt-wrong-dpop-signature") + public Response callbackWrongDpopSignature(@QueryParam("code") String code) throws Exception { + return callProtectedEndpoint(code, "dpop-jwt", "callback-jwt-wrong-dpop-signature", "GET", "dpop-jwt", + "dpop-jwt", true, false, false); + } + + @GET + @Path("login-jwt-wrong-dpop-jwk-key") + public Response loginJwtWrongDpopJwkKey() { + return redirect("dpop-jwt", "callback-jwt-wrong-dpop-jwk-key"); + } + + @GET + @Path("callback-jwt-wrong-dpop-jwk-key") + public Response callbackWrongDpopJwkKey(@QueryParam("code") String code) throws Exception { + return callProtectedEndpoint(code, "dpop-jwt", "callback-jwt-wrong-dpop-jwk-key", "GET", "dpop-jwt-wrong-uri", + "dpop-jwt", false, true, false); + } + + @GET + @Path("login-jwt-wrong-dpop-token-hash") + public Response loginJwtWrongDpopTokenHash() { + return redirect("dpop-jwt", "callback-jwt-wrong-dpop-token-hash"); + } + + @GET + @Path("callback-jwt-wrong-dpop-token-hash") + public Response callbackWrongDpopTokenHash(@QueryParam("code") String code) throws Exception { + return callProtectedEndpoint(code, "dpop-jwt", "callback-jwt-wrong-dpop-token-hash", "GET", "dpop-jwt", + "dpop-jwt", false, false, true); + } + + private Response callProtectedEndpoint(String code, String tenantId, String redirectPath, String dPopHttpMethod, + String dPopEndpointPath, String quarkusEndpointPath, + boolean wrongDpopSignature, + boolean wrongDpopJwkKey, + boolean wrongDpopTokenHash) + throws Exception { + String redirectUriParam = ui.getBaseUriBuilder().path("single-page-app").path(redirectPath).build().toString(); + + MultiMap grantParams = MultiMap.caseInsensitiveMultiMap(); + grantParams.add(OidcConstants.CLIENT_ID, "backend-service"); + grantParams.add(OidcConstants.GRANT_TYPE, OidcConstants.AUTHORIZATION_CODE); + grantParams.add(OidcConstants.CODE_FLOW_CODE, code); + grantParams.add(OidcConstants.CODE_FLOW_REDIRECT_URI, redirectUriParam); + + Buffer encoded = OidcCommonUtils.encodeForm(grantParams); + HttpRequest requestToKeycloak = client.postAbs(getConfigMetadata(tenantId).getTokenUri()); + requestToKeycloak.putHeader("Content-Type", HttpHeaders.APPLICATION_X_WWW_FORM_URLENCODED.toString()); + + KeyPair keyPair = KeyUtils.generateKeyPair(2048); + requestToKeycloak.putHeader("DPoP", createDPopProofForKeycloak(keyPair, tenantId)); + + JsonObject grantResponse = requestToKeycloak.sendBufferAndAwait(encoded).bodyAsJsonObject(); + String accessToken = grantResponse.getString("access_token"); + + String requestPath = ui.getBaseUriBuilder().path("service").path(quarkusEndpointPath).build().toString(); + HttpRequest requestToQuarkus = client.getAbs(requestPath); + requestToQuarkus.putHeader("Accept", "text/plain"); + String absoluteDpopEndpointUri = ui.getBaseUriBuilder().path("service").path(dPopEndpointPath).build().toString(); + requestToQuarkus.putHeader("DPoP", createDPopProofForQuarkus(keyPair, accessToken, dPopHttpMethod, + absoluteDpopEndpointUri, wrongDpopSignature, wrongDpopJwkKey, wrongDpopTokenHash)); + requestToQuarkus.putHeader("Authorization", "DPoP " + accessToken); + HttpResponse response = requestToQuarkus.sendAndAwait(); + return Response.ok(response.bodyAsString()).build(); + } + + private Response redirect(String tenantId, String callbackPath) { + StringBuilder codeFlowParams = new StringBuilder(); + + // response_type + codeFlowParams.append(OidcConstants.CODE_FLOW_RESPONSE_TYPE).append("=").append(OidcConstants.CODE_FLOW_CODE); + // client_id + codeFlowParams.append("&").append(OidcConstants.CLIENT_ID).append("=").append("backend-service"); + // scope + codeFlowParams.append("&").append(OidcConstants.TOKEN_SCOPE).append("=").append("openid"); + // redirect_uri + String redirectUriParam = ui.getBaseUriBuilder().path("single-page-app/" + callbackPath).build().toString(); + codeFlowParams.append("&").append(OidcConstants.CODE_FLOW_REDIRECT_URI).append("=") + .append(OidcCommonUtils.urlEncode(redirectUriParam)); + // state + String state = UUID.randomUUID().toString(); + codeFlowParams.append("&").append(OidcConstants.CODE_FLOW_STATE).append("=").append(state); + return Response + .seeOther(URI.create(getConfigMetadata(tenantId).getAuthorizationUri() + "?" + codeFlowParams.toString())) + .cookie(new NewCookie("state", state)) + .build(); + } + + @PreDestroy + void close() { + client.close(); + } + + private String createDPopProofForKeycloak(KeyPair keyPair, String tenantId) throws Exception { + + return Jwt.claim("htm", "POST") + .claim("htu", getConfigMetadata(tenantId).getTokenUri()) + .jws() + .header("typ", "dpop+jwt") + .jwk(keyPair.getPublic()) + .sign(keyPair.getPrivate()); + } + + private String createDPopProofForQuarkus(KeyPair keyPair, String accessToken, String dPopHttpMethod, + String dPopEndpointPath, + boolean wrongDpopSignature, + boolean wrongDpopJwkKey, + boolean wrongAccesstokenHash) throws Exception { + + JwtClaimsBuilder jwtClaimsBuilder = Jwt.claim("htm", dPopHttpMethod) + .claim("htu", dPopEndpointPath); + JwtSignatureBuilder jwtSignatureBuilder = jwtClaimsBuilder + .claim("ath", wrongAccesstokenHash ? accessToken + : OidcCommonUtils.base64UrlEncode( + OidcUtils.getSha256Digest(accessToken))) + .jws() + .header("typ", "dpop+jwt"); + + jwtSignatureBuilder = wrongDpopJwkKey ? jwtSignatureBuilder.jwk(KeyUtils.generateKeyPair(2048).getPublic()) + : jwtSignatureBuilder.jwk(keyPair.getPublic()); + + PrivateKey signingKey = wrongDpopSignature ? KeyUtils.generateKeyPair(2048).getPrivate() + : keyPair.getPrivate(); + return jwtSignatureBuilder.sign(signingKey); + } + + private OidcConfigurationMetadata getConfigMetadata(String tenantId) { + return oidcTenants.getStaticTenant(tenantId).getOidcMetadata(); + } +} diff --git a/integration-tests/oidc-dpop/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java b/integration-tests/oidc-dpop/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java new file mode 100644 index 0000000000000..808f34b706acf --- /dev/null +++ b/integration-tests/oidc-dpop/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java @@ -0,0 +1,41 @@ +package io.quarkus.it.keycloak; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; + +import org.eclipse.microprofile.jwt.JsonWebToken; + +import io.quarkus.oidc.runtime.OidcUtils; +import io.quarkus.security.Authenticated; +import io.vertx.ext.web.RoutingContext; + +@Path("/service") +@Authenticated +public class ProtectedResource { + + @Inject + JsonWebToken principal; + + @Inject + RoutingContext routingContext; + + @GET + @Produces("text/plain") + @Path("dpop-jwt") + public String hello() { + return "Hello, " + principal.getName() + "; " + + "JWK thumbprint in JWT: " + isJwtTokenThumbprintAvailable() + ", " + + "JWK thumbprint in introspection: " + isIntrospectionThumbprintAvailable(); + } + + private boolean isJwtTokenThumbprintAvailable() { + return Boolean.TRUE.equals(routingContext.get(OidcUtils.DPOP_JWT_THUMBPRINT)); + } + + private boolean isIntrospectionThumbprintAvailable() { + + return Boolean.TRUE.equals(routingContext.get(OidcUtils.DPOP_INTROSPECTION_THUMBPRINT)); + } +} diff --git a/integration-tests/oidc-dpop/src/main/resources/application.properties b/integration-tests/oidc-dpop/src/main/resources/application.properties new file mode 100644 index 0000000000000..73f552908b13e --- /dev/null +++ b/integration-tests/oidc-dpop/src/main/resources/application.properties @@ -0,0 +1,10 @@ +# Disable default tenant +quarkus.oidc.tenant-enabled=false +quarkus.keycloak.devservices.start-with-disabled-tenant=true +quarkus.keycloak.devservices.realm-path=quarkus-realm.json +quarkus.keycloak.devservices.features=dpop + +quarkus.oidc.dpop-jwt.auth-server-url=${quarkus.oidc.auth-server-url} +quarkus.oidc.dpop-jwt.token.authorization-scheme=DPoP + +quarkus.log.category."org.htmlunit".level=ERROR diff --git a/integration-tests/oidc-dpop/src/main/resources/quarkus-realm.json b/integration-tests/oidc-dpop/src/main/resources/quarkus-realm.json new file mode 100644 index 0000000000000..5b1a86aad740d --- /dev/null +++ b/integration-tests/oidc-dpop/src/main/resources/quarkus-realm.json @@ -0,0 +1,2366 @@ +{ + "id": "quarkus", + "realm": "quarkus", + "notBefore": 0, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxTemporaryLockouts": 0, + "bruteForceStrategy": "MULTIPLE", + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "roles": { + "realm": [ + { + "id": "3ce83241-464b-4ca0-8f0f-17002a797aab", + "name": "admin", + "composite": false, + "clientRole": false, + "containerId": "quarkus", + "attributes": {} + }, + { + "id": "68615956-51ca-49ca-865a-f9cb2571b027", + "name": "confidential", + "composite": false, + "clientRole": false, + "containerId": "quarkus", + "attributes": {} + }, + { + "id": "c6d57a00-eb97-460d-91b0-89e6a94a7aa5", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "quarkus", + "attributes": {} + }, + { + "id": "6607d6c4-60c7-412e-badc-8e86e1925fb0", + "name": "default-roles-quarkus", + "description": "${role_default-roles}", + "composite": false, + "clientRole": false, + "containerId": "quarkus", + "attributes": {} + }, + { + "id": "c50286f6-3562-473f-ad45-9767b982ff45", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "quarkus", + "attributes": {} + }, + { + "id": "d3246456-8f5d-4722-8364-a46a8d25dc7c", + "name": "user", + "composite": false, + "clientRole": false, + "containerId": "quarkus", + "attributes": {} + } + ], + "client": { + "realm-management": [ + { + "id": "4b24739e-3a0a-48d2-b202-713430d775d2", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "1238e880-907f-4e8b-a032-4d09a922adf8", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "bcc6637a-294c-4529-a706-33b8c49f40fc", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-groups", + "query-users" + ] + } + }, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "f65a9a54-d689-4c45-87cd-f177babdeaef", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "183e58f4-136b-4c91-b20a-5c76857a671e", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "9aec187f-d623-45c7-a8b3-5aa32d115f50", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "52521d81-e7d6-4929-95cb-0a084c5bacb8", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "e92c753a-7b17-4adc-9962-04f24040e404", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "1285d11d-08f4-4753-b27e-d5f7b0e76fca", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "b0ee027f-5aa6-48eb-837f-4635590576ec", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "61ac3405-ccbd-4cdf-8cac-c918e1d77e1f", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "f1176efb-e24b-4fab-8b37-8265aefd10e1", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "968be265-6868-416a-91a1-e5bd882349ab", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "e77611fc-5ec5-4438-96c3-b291aae78d0c", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "165b24e1-9488-4cc7-87cd-e74b1cdc5619", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "f5163480-f5fc-4355-8be1-8cc96ff7d99d", + "name": "realm-admin", + "description": "${role_realm-admin}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "manage-identity-providers", + "query-clients", + "view-users", + "view-identity-providers", + "view-events", + "view-clients", + "manage-events", + "query-realms", + "manage-clients", + "view-authorization", + "query-groups", + "query-users", + "manage-authorization", + "manage-users", + "manage-realm", + "create-client", + "view-realm", + "impersonation" + ] + } + }, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "64ec1233-2cee-4d9b-ab6f-0bd06702c684", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "6e633885-b1fb-4ca8-9ef9-7c4c8f8732e8", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "683bddad-81c6-4dca-87b6-e14b0b2ae524", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + } + ], + "security-admin-console": [], + "admin-cli": [], + "account-console": [], + "backend-service": [ + { + "id": "5b9947c6-eb74-4de6-8623-0285720993f3", + "name": "uma_protection", + "composite": false, + "clientRole": true, + "containerId": "302430aa-3929-42cf-8ba2-2b9d2e71dc3a", + "attributes": {} + } + ], + "broker": [ + { + "id": "bee1f77b-34a9-4386-9eca-eb19db248394", + "name": "read-token", + "description": "${role_read-token}", + "composite": false, + "clientRole": true, + "containerId": "2a02328b-6aa6-49a8-b56c-7036c273c70b", + "attributes": {} + } + ], + "account": [ + { + "id": "c84d374e-ee49-47e0-b65a-21519e7f3a99", + "name": "delete-account", + "description": "${role_delete-account}", + "composite": false, + "clientRole": true, + "containerId": "35b5a50f-a32a-4bd1-b4b3-50f0ade135c7", + "attributes": {} + }, + { + "id": "fee1f70e-3a5d-4598-b5a3-6ff9ee208836", + "name": "manage-consent", + "description": "${role_manage-consent}", + "composite": true, + "composites": { + "client": { + "account": [ + "view-consent" + ] + } + }, + "clientRole": true, + "containerId": "35b5a50f-a32a-4bd1-b4b3-50f0ade135c7", + "attributes": {} + }, + { + "id": "97a0dbe6-618d-4f8b-997a-9802467b4350", + "name": "view-consent", + "description": "${role_view-consent}", + "composite": false, + "clientRole": true, + "containerId": "35b5a50f-a32a-4bd1-b4b3-50f0ade135c7", + "attributes": {} + }, + { + "id": "9a2ff099-0b69-444c-9c4d-801c5eda630c", + "name": "view-groups", + "description": "${role_view-groups}", + "composite": false, + "clientRole": true, + "containerId": "35b5a50f-a32a-4bd1-b4b3-50f0ade135c7", + "attributes": {} + }, + { + "id": "d3ffeda8-8d57-4b63-ae1d-90f88bc4b068", + "name": "manage-account-links", + "description": "${role_manage-account-links}", + "composite": false, + "clientRole": true, + "containerId": "35b5a50f-a32a-4bd1-b4b3-50f0ade135c7", + "attributes": {} + }, + { + "id": "1ffcc7fe-50a8-4300-b172-10f651e5a5bd", + "name": "view-profile", + "description": "${role_view-profile}", + "composite": false, + "clientRole": true, + "containerId": "35b5a50f-a32a-4bd1-b4b3-50f0ade135c7", + "attributes": {} + }, + { + "id": "74f86380-8e18-407f-ad16-529044f9c7dc", + "name": "manage-account", + "description": "${role_manage-account}", + "composite": true, + "composites": { + "client": { + "account": [ + "manage-account-links" + ] + } + }, + "clientRole": true, + "containerId": "35b5a50f-a32a-4bd1-b4b3-50f0ade135c7", + "attributes": {} + }, + { + "id": "498c752e-d7eb-442a-bc48-f0013665179d", + "name": "view-applications", + "description": "${role_view-applications}", + "composite": false, + "clientRole": true, + "containerId": "35b5a50f-a32a-4bd1-b4b3-50f0ade135c7", + "attributes": {} + } + ] + } + }, + "groups": [], + "defaultRole": { + "id": "6607d6c4-60c7-412e-badc-8e86e1925fb0", + "name": "default-roles-quarkus", + "description": "${role_default-roles}", + "composite": false, + "clientRole": false, + "containerId": "quarkus" + }, + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpPolicyCodeReusable": false, + "otpSupportedApplications": [ + "totpAppFreeOTPName", + "totpAppGoogleName", + "totpAppMicrosoftAuthenticatorName" + ], + "localizationTexts": {}, + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyExtraOrigins": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256", + "RS256" + ], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "webAuthnPolicyPasswordlessExtraOrigins": [], + "clientScopeMappings": { + "account": [ + { + "client": "account-console", + "roles": [ + "manage-account", + "view-groups" + ] + } + ] + }, + "clients": [ + { + "id": "35b5a50f-a32a-4bd1-b4b3-50f0ade135c7", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/quarkus/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": [ + "/realms/quarkus/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "realm_client": "false", + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "d8adbd75-4d35-4951-84a6-036359747daf", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/quarkus/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/quarkus/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "realm_client": "false", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "79f82b8d-e4e0-4c6a-bf4d-5d2118393461", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "c6e812f9-326b-4e66-9197-157a5d43b172", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "realm_client": "false", + "client.use.lightweight.access.token.enabled": "true", + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "302430aa-3929-42cf-8ba2-2b9d2e71dc3a", + "clientId": "backend-service", + "name": "", + "description": "", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "realm_client": "false", + "oidc.ciba.grant.enabled": "false", + "backchannel.logout.session.required": "true", + "post.logout.redirect.uris": "+", + "oauth2.device.authorization.grant.enabled": "false", + "display.on.consent.screen": "false", + "backchannel.logout.revoke.offline.tokens": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "id": "1390addb-ba10-4455-a1ea-8455c3770cf1", + "name": "Client ID", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientId", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientId", + "jsonType.label": "String", + "userinfo.token.claim": "true" + } + }, + { + "id": "cdafda09-f6d9-41e3-87ef-6789e861689a", + "name": "Client Host", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientHost", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientHost", + "jsonType.label": "String", + "userinfo.token.claim": "true" + } + }, + { + "id": "95b47211-912c-43f5-84ce-5bfbc761325d", + "name": "Client IP Address", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientAddress", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientAddress", + "jsonType.label": "String", + "userinfo.token.claim": "true" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "2a02328b-6aa6-49a8-b56c-7036c273c70b", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "realm_client": "true", + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "dd29e998-54e9-4067-884e-4f986e990c1d", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "realm_client": "true", + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "6517b152-0693-4b28-a798-a0deea3e8644", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/quarkus/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/admin/quarkus/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "realm_client": "false", + "client.use.lightweight.access.token.enabled": "true", + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "9c7093a9-4da1-47e4-b2a5-afe180782220", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String", + "userinfo.token.claim": "true" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + } + ], + "clientScopes": [ + { + "id": "0d8dbb0f-0d91-40bf-a263-2706457aebe8", + "name": "acr", + "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "247c235a-181b-43e8-ad41-9346ef74845c", + "name": "acr loa level", + "protocol": "openid-connect", + "protocolMapper": "oidc-acr-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "id": "83e275f7-b171-45fa-99c7-7c04f91fbe41", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "consent.screen.text": "${rolesScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "9eb470cc-8157-46f2-8233-8cae169c6591", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "eebdefd0-c446-4bf3-b945-08db42f0ea92", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + }, + { + "id": "37c62d93-c670-487c-8c3a-a6329a9924b0", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String", + "multivalued": "true" + } + } + ] + }, + { + "id": "7eaa8ede-9a92-487a-9444-60a5d7355542", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "e7616dd3-8886-4d47-8645-74e4565d7606", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + }, + { + "id": "35bfd94e-681f-456a-bca0-0d0d8d986a96", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${addressScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "1f710637-5a3c-45f3-b4d3-74046993e0eb", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "eb0bdf87-6cda-4684-89a8-f7bd6f0c7bba", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${emailScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "1ea39fbb-c692-4a1d-a143-a05b030889cb", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String", + "userinfo.token.claim": "true" + } + }, + { + "id": "f97bd1de-6c95-4c5b-804c-f8b354457453", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean", + "userinfo.token.claim": "true" + } + } + ] + }, + { + "id": "58e57c6f-18bf-4347-9ab0-b8325ef522e0", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "consent.screen.text": "", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "5a4a2c20-fef2-40b5-9406-136475442b47", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": {} + } + ] + }, + { + "id": "97aca0c9-7f14-4783-bb48-681de54f0b31", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "55621a1e-cd6b-45a7-9f06-a678e0801b9c", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "6c4f32b0-8ae4-4b4b-b4fa-a053df0bbb3a", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "userinfo.token.claim": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + }, + { + "id": "2687cb87-1dbf-435c-8ef9-f2fe38127405", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String", + "userinfo.token.claim": "true" + } + } + ] + }, + { + "id": "6e788b2f-e327-472d-97a0-5ecd218b773c", + "name": "basic", + "description": "OpenID Connect scope for add all basic claims to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "f4b5b2bd-b926-458f-9067-a28f2d0f67c6", + "name": "auth_time", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "AUTH_TIME", + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "auth_time", + "jsonType.label": "long" + } + }, + { + "id": "6357c42c-8349-4555-83d0-777cacdf3455", + "name": "sub", + "protocol": "openid-connect", + "protocolMapper": "oidc-sub-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "id": "d20498e8-4ec8-4496-9d8f-c09131dd5d15", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${profileScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "7da35ca7-5c93-4d23-b6b7-761d80c966c8", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String", + "userinfo.token.claim": "true" + } + }, + { + "id": "a443a633-7cd2-406d-85f1-6e3d3173eff9", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String", + "userinfo.token.claim": "true" + } + }, + { + "id": "d04d2dd6-04fc-4230-90eb-7074056cfdee", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String", + "userinfo.token.claim": "true" + } + }, + { + "id": "ef68a07b-ed0a-418b-9c6d-7ecd58946813", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "String", + "userinfo.token.claim": "true" + } + }, + { + "id": "144acdba-ee08-4349-b806-a4394bd5f351", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String", + "userinfo.token.claim": "true" + } + }, + { + "id": "4b435d62-1f62-4513-a131-208318731d7b", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String", + "userinfo.token.claim": "true" + } + }, + { + "id": "794b162d-460a-4465-b90d-66dabc4b3cce", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String", + "userinfo.token.claim": "true" + } + }, + { + "id": "779b131a-d0cc-420d-90b3-075b19210379", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String", + "userinfo.token.claim": "true" + } + }, + { + "id": "0e0f1e8d-60f9-4435-b753-136d70e56af8", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String", + "userinfo.token.claim": "true" + } + }, + { + "id": "8451d26b-904d-4858-9db1-87fe137c1172", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String", + "userinfo.token.claim": "true" + } + }, + { + "id": "011fe224-355f-4e3c-a3d4-6a325eec561d", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String", + "userinfo.token.claim": "true" + } + }, + { + "id": "06f656a1-67f1-4c53-92df-9e5823853191", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "03293b81-5599-4163-81b8-eb05c3d14ed2", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String", + "userinfo.token.claim": "true" + } + }, + { + "id": "d21642b7-8190-4de4-8d0d-09b0e505c02c", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String", + "userinfo.token.claim": "true" + } + } + ] + }, + { + "id": "541f2eae-d481-4d00-be30-89f4f60d169f", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${phoneScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "eda935c3-7294-403c-85bd-fee7216af822", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String", + "userinfo.token.claim": "true" + } + }, + { + "id": "0b8c0161-5042-4912-a753-c262569ed5bc", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean", + "userinfo.token.claim": "true" + } + } + ] + } + ], + "defaultDefaultClientScopes": [ + "web-origins", + "role_list", + "roles", + "profile", + "email", + "acr", + "basic" + ], + "defaultOptionalClientScopes": [ + "address", + "phone", + "microprofile-jwt", + "offline_access" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "referrerPolicy": "no-referrer", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "7ebad719-3c5e-4880-a9f1-3242dd9dbe24", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "8fe9bd3a-a11c-4c97-948e-90ba7fbe008f", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-attribute-mapper", + "oidc-full-name-mapper", + "saml-user-property-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-usermodel-property-mapper", + "oidc-sha256-pairwise-sub-mapper", + "saml-role-list-mapper", + "oidc-address-mapper" + ] + } + }, + { + "id": "e9b76eee-365f-4b5f-80cb-316eb07b36fa", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": [ + "200" + ] + } + }, + { + "id": "8ed9d103-7a79-47b4-9426-9e4a84340d22", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "a07e90f1-5662-4344-8529-f284c361a25e", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "9b4e5b69-1d07-489b-b8a5-07329c957141", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] + } + }, + { + "id": "e2f513d3-44e3-435c-8b2a-68a5d384fd97", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "43a5aac2-b395-4935-94cb-12f4d9b4eb05", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-attribute-mapper", + "oidc-usermodel-property-mapper", + "saml-user-property-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-address-mapper", + "saml-role-list-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-full-name-mapper" + ] + } + } + ], + "org.keycloak.userprofile.UserProfileProvider": [ + { + "id": "083eacd8-ab82-47fe-8743-55121911bd5e", + "providerId": "declarative-user-profile", + "subComponents": {}, + "config": { + "kc.user.profile.config": [ + "{\"attributes\":[{\"name\":\"username\",\"displayName\":\"${username}\",\"validations\":{\"length\":{\"min\":3,\"max\":255},\"username-prohibited-characters\":{},\"up-username-not-idn-homograph\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"email\",\"displayName\":\"${email}\",\"validations\":{\"email\":{},\"length\":{\"max\":255}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"firstName\",\"displayName\":\"${firstName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"lastName\",\"displayName\":\"${lastName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false}],\"groups\":[{\"name\":\"user-metadata\",\"displayHeader\":\"User metadata\",\"displayDescription\":\"Attributes, which refer to user metadata\"}],\"unmanagedAttributePolicy\":\"ENABLED\"}" + ] + } + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "b9b879cf-66e2-48df-adb5-3b61e1f39bc7", + "name": "hmac-generated-hs512", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "HS512" + ] + } + }, + { + "id": "066f8625-06ba-4463-995f-93a058d2d800", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "19c225cc-b499-48b1-aed6-3e1dd5bcf04c", + "name": "hmac-generated", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "HS256" + ] + } + }, + { + "id": "4008d665-26c4-4056-a028-232bc0636029", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "55f3ddc5-0f36-496d-817f-3aa8f426ee45", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false + } + ] + }, + { + "id": "2d0ccc2f-888c-495f-91ae-dfffba572d33", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "b7ff5812-2bc2-4f8f-9913-bd3b97a08618", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "ddbfb446-21d8-44c2-a207-7f83d760e94f", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "21dc8a77-3900-46e7-b1e4-40f5bcbd9b8e", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Account verification options", + "userSetupAllowed": false + } + ] + }, + { + "id": "329ed4e1-d3a8-42aa-a9ff-991a0e8f2851", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "66b4a633-6ba0-41e2-944f-0b13369c1e78", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false + } + ] + }, + { + "id": "fce169a3-c245-4dc8-a3c5-295bfa7057a4", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "4c5476fa-9aef-440b-bd14-25bf8cbfcd16", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "forms", + "userSetupAllowed": false + } + ] + }, + { + "id": "75d65771-3bfb-4def-a539-656de7d1af58", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "a6a9036b-192e-461f-91c7-d8117435188d", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "f86bdf88-8bee-480b-8e81-67dcd674e46c", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "6f87019e-c995-4049-b8bf-d08a9c3a13f3", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "User creation or linking", + "userSetupAllowed": false + } + ] + }, + { + "id": "fadc7c73-7fae-4c28-ad69-51bb03ba17bf", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "d930f23e-ae58-45b2-9e01-20691200c926", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": true, + "flowAlias": "registration form", + "userSetupAllowed": false + } + ] + }, + { + "id": "8d62b1dd-6066-454d-bc76-f783d50fecaa", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "f99be349-ce0b-44a4-9f70-73f57cb8c164", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "autheticatorFlow": true, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "33ee7503-bd12-4e5a-903c-5ae580f48709", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "6970ebc8-0b24-414c-8544-3cc48b1a0e4c", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "d14b76f4-b608-4b13-b51c-b9e162ad784b", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "TERMS_AND_CONDITIONS", + "name": "Terms and Conditions", + "providerId": "TERMS_AND_CONDITIONS", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "delete_credential", + "name": "Delete Credential", + "providerId": "delete_credential", + "enabled": true, + "defaultAction": false, + "priority": 100, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "firstBrokerLoginFlow": "first broker login", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaExpiresIn": "120", + "cibaAuthRequestedUserHint": "login_hint", + "oauth2DeviceCodeLifespan": "600", + "oauth2DevicePollingInterval": "5", + "parRequestUriLifespan": "60", + "cibaInterval": "5", + "realmReusableOtpCode": "false" + }, + "users" : [ { + "id" : "af134cab-f41c-4675-b141-205f975db679", + "username" : "admin", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "credentials" : [ { + "type" : "password", + "hashedSaltedValue" : "NICTtwsvSxJ5hL8hLAuleDUv9jwZcuXgxviMXvR++cciyPtiIEStEaJUyfA9DOir59awjPrHOumsclPVjNBplA==", + "salt" : "T/2P5o5oxFJUEk68BRURRg==", + "hashIterations" : 27500, + "counter" : 0, + "algorithm" : "pbkdf2-sha256", + "digits" : 0, + "period" : 0, + "createdDate" : 1554245879354, + "config" : { } + } ], + "disableableCredentialTypes" : [ "password" ], + "requiredActions" : [ ], + "realmRoles" : [ "admin", "user" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "eb4123a3-b722-4798-9af5-8957f823657a", + "username" : "alice", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "credentials" : [ { + "type" : "password", + "hashedSaltedValue" : "A3okqV2T/ybXTVEgKfosoSjP8Yc9IZbFP/SY4cEd6hag7TABQrQ6nUSuwagGt96l8cw1DTijO75PqX6uiTXMzw==", + "salt" : "sl4mXx6T9FypPH/s9TngfQ==", + "hashIterations" : 27500, + "counter" : 0, + "algorithm" : "pbkdf2-sha256", + "digits" : 0, + "period" : 0, + "createdDate" : 1554245879116, + "config" : { } + } ], + "disableableCredentialTypes" : [ "password" ], + "requiredActions" : [ ], + "realmRoles" : [ "user" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "1eed6a8e-a853-4597-b4c6-c4c2533546a0", + "username" : "jdoe", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "credentials" : [ { + "type" : "password", + "hashedSaltedValue" : "JV3DUNLjqOadjbBOtC4rvacQI553CGaDGAzBS8MR5ReCr7SwF3E6CsW3T7/XO8ITZAsch8+A/6loeuCoVLLJrg==", + "salt" : "uCbOH7HZtyDtMd0E9DG/nw==", + "hashIterations" : 27500, + "counter" : 0, + "algorithm" : "pbkdf2-sha256", + "digits" : 0, + "period" : 0, + "createdDate" : 1554245879227, + "config" : { } + } ], + "disableableCredentialTypes" : [ "password" ], + "requiredActions" : [ ], + "realmRoles" : [ "confidential", "user" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "948c59ec-46ed-4d99-aa43-02900029b930", + "createdTimestamp" : 1554245880023, + "username" : "service-account-backend-service", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "email" : "service-account-backend-service@placeholder.org", + "serviceAccountClientId" : "backend-service", + "credentials" : [ ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "offline_access" ], + "clientRoles" : { + "backend-service" : [ "uma_protection" ], + "account" : [ "view-profile", "manage-account" ] + }, + "notBefore" : 0, + "groups" : [ ] + } ], + "keycloakVersion": "26.0.7", + "userManagedAccessAllowed": false, + "organizationsEnabled": false, + "clientProfiles": { + "profiles": [] + }, + "clientPolicies": { + "policies": [] + } +} diff --git a/integration-tests/oidc-dpop/src/test/java/io/quarkus/it/keycloak/OidcDPopInGraalITCase.java b/integration-tests/oidc-dpop/src/test/java/io/quarkus/it/keycloak/OidcDPopInGraalITCase.java new file mode 100644 index 0000000000000..51a9fa892627a --- /dev/null +++ b/integration-tests/oidc-dpop/src/test/java/io/quarkus/it/keycloak/OidcDPopInGraalITCase.java @@ -0,0 +1,7 @@ +package io.quarkus.it.keycloak; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class OidcDPopInGraalITCase extends OidcDPopTest { +} diff --git a/integration-tests/oidc-dpop/src/test/java/io/quarkus/it/keycloak/OidcDPopTest.java b/integration-tests/oidc-dpop/src/test/java/io/quarkus/it/keycloak/OidcDPopTest.java new file mode 100644 index 0000000000000..7f31034c4ab46 --- /dev/null +++ b/integration-tests/oidc-dpop/src/test/java/io/quarkus/it/keycloak/OidcDPopTest.java @@ -0,0 +1,149 @@ +package io.quarkus.it.keycloak; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +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.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class OidcDPopTest { + + @Test + public void testDPopProof() throws Exception { + try (final WebClient webClient = createWebClient()) { + HtmlPage page = webClient.getPage("http://localhost:8081/single-page-app/login-jwt"); + + assertEquals("Sign in to quarkus", page.getTitleText()); + + HtmlForm loginForm = page.getForms().get(0); + + loginForm.getInputByName("username").setValueAttribute("alice"); + loginForm.getInputByName("password").setValueAttribute("alice"); + + TextPage textPage = loginForm.getButtonByName("login").click(); + + assertEquals("Hello, alice; JWK thumbprint in JWT: true, JWK thumbprint in introspection: false", + textPage.getContent()); + + webClient.getCookieManager().clearCookies(); + } + } + + @Test + public void testDPopProofWrongHttpMethod() throws Exception { + try (final WebClient webClient = createWebClient()) { + HtmlPage page = webClient.getPage("http://localhost:8081/single-page-app/login-jwt-wrong-dpop-http-method"); + + assertEquals("Sign in to quarkus", page.getTitleText()); + + HtmlForm loginForm = page.getForms().get(0); + + loginForm.getInputByName("username").setValueAttribute("alice"); + loginForm.getInputByName("password").setValueAttribute("alice"); + + TextPage textPage = loginForm.getButtonByName("login").click(); + + assertEquals("401 status from ProtectedResource", + textPage.getContent()); + + webClient.getCookieManager().clearCookies(); + } + } + + @Test + public void testDPopProofWrongHttpUri() throws Exception { + try (final WebClient webClient = createWebClient()) { + HtmlPage page = webClient.getPage("http://localhost:8081/single-page-app/login-jwt-wrong-dpop-http-uri"); + + assertEquals("Sign in to quarkus", page.getTitleText()); + + HtmlForm loginForm = page.getForms().get(0); + + loginForm.getInputByName("username").setValueAttribute("alice"); + loginForm.getInputByName("password").setValueAttribute("alice"); + + TextPage textPage = loginForm.getButtonByName("login").click(); + + assertEquals("401 status from ProtectedResource", + textPage.getContent()); + + webClient.getCookieManager().clearCookies(); + } + } + + @Test + public void testDPopProofWrongSignature() throws Exception { + try (final WebClient webClient = createWebClient()) { + HtmlPage page = webClient.getPage("http://localhost:8081/single-page-app/login-jwt-wrong-dpop-signature"); + + assertEquals("Sign in to quarkus", page.getTitleText()); + + HtmlForm loginForm = page.getForms().get(0); + + loginForm.getInputByName("username").setValueAttribute("alice"); + loginForm.getInputByName("password").setValueAttribute("alice"); + + TextPage textPage = loginForm.getButtonByName("login").click(); + + assertEquals("401 status from ProtectedResource", + textPage.getContent()); + + webClient.getCookieManager().clearCookies(); + } + } + + @Test + public void testDPopProofWrongJwkKey() throws Exception { + try (final WebClient webClient = createWebClient()) { + HtmlPage page = webClient.getPage("http://localhost:8081/single-page-app/login-jwt-wrong-dpop-jwk-key"); + + assertEquals("Sign in to quarkus", page.getTitleText()); + + HtmlForm loginForm = page.getForms().get(0); + + loginForm.getInputByName("username").setValueAttribute("alice"); + loginForm.getInputByName("password").setValueAttribute("alice"); + + TextPage textPage = loginForm.getButtonByName("login").click(); + + assertEquals("401 status from ProtectedResource", + textPage.getContent()); + + webClient.getCookieManager().clearCookies(); + } + } + + @Test + public void testDPopProofWrongTokenHash() throws Exception { + try (final WebClient webClient = createWebClient()) { + HtmlPage page = webClient.getPage("http://localhost:8081/single-page-app/login-jwt-wrong-dpop-token-hash"); + + assertEquals("Sign in to quarkus", page.getTitleText()); + + HtmlForm loginForm = page.getForms().get(0); + + loginForm.getInputByName("username").setValueAttribute("alice"); + loginForm.getInputByName("password").setValueAttribute("alice"); + + TextPage textPage = loginForm.getButtonByName("login").click(); + + assertEquals("401 status from ProtectedResource", + textPage.getContent()); + + webClient.getCookieManager().clearCookies(); + } + } + + private WebClient createWebClient() { + WebClient webClient = new WebClient(); + webClient.setCssErrorHandler(new SilentCssErrorHandler()); + return webClient; + } + +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index bbfa3d6ea4f3b..5d297a6148331 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -261,6 +261,7 @@ oidc-client-wiremock oidc-dev-services oidc-mtls + oidc-dpop oidc-token-propagation oidc-token-propagation-reactive openapi From 64b82a5e76442eabf279ceb9845e29ac2f3d60d4 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Thu, 13 Feb 2025 09:08:43 +0200 Subject: [PATCH 13/16] Add a way to override the artifact type to test for `@QuarkusIntegrationTest` This can be useful when the application builds a container image as it's final artifact, but would like to use `@QuarkusIntegrationTest` to test the jar or native image based on which the container was built Closes: #46232 (cherry picked from commit f7ee073542262885a42b6a9532b815676959ac06) --- .../deployment/dev/testing/TestConfig.java | 10 ++++++ .../runner/bootstrap/AugmentActionImpl.java | 32 +++++++++++++++---- .../test/junit/IntegrationTestUtil.java | 9 ++++++ .../QuarkusIntegrationTestExtension.java | 6 ++-- .../QuarkusMainIntegrationTestExtension.java | 7 ++-- 5 files changed, 50 insertions(+), 14 deletions(-) diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestConfig.java index 02abd53733599..7065b9a154716 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestConfig.java @@ -261,6 +261,16 @@ public interface TestConfig { @WithDefault("false") boolean enableCallbacksForIntegrationTests(); + /** + * Used to override the artifact type against which a {@code @QuarkusIntegrationTest} or {@code @QuarkusMainIntegrationTest} + * run. + * For example, if the application's artifact is a container build from a jar, this property could be used to test the jar + * instead of the container. + *

+ * Allowed values are: jar, native + */ + Optional integrationTestArtifactType(); + interface Profile { /** * A comma separated list of profiles (dev, test, prod or custom profiles) to use when testing using @QuarkusTest diff --git a/core/deployment/src/main/java/io/quarkus/runner/bootstrap/AugmentActionImpl.java b/core/deployment/src/main/java/io/quarkus/runner/bootstrap/AugmentActionImpl.java index 27782e15db666..28d0bdb5096a7 100644 --- a/core/deployment/src/main/java/io/quarkus/runner/bootstrap/AugmentActionImpl.java +++ b/core/deployment/src/main/java/io/quarkus/runner/bootstrap/AugmentActionImpl.java @@ -190,8 +190,7 @@ public AugmentResult createProductionApplication() { if (artifactResultBuildItems.isEmpty()) { throw new IllegalStateException("No artifact results were produced"); } - ArtifactResultBuildItem lastResult = artifactResultBuildItems.get(artifactResultBuildItems.size() - 1); - writeArtifactResultMetadataFile(buildSystemTargetBuildItem, lastResult); + writeArtifactResultMetadataFile(buildSystemTargetBuildItem, artifactResultBuildItems); return new AugmentResult(artifactResultBuildItems.stream() .map(a -> new ArtifactResult(a.getPath(), a.getType(), a.getMetadata())) @@ -241,14 +240,28 @@ private void writeDebugSourceFile(BuildResult result) { } private void writeArtifactResultMetadataFile(BuildSystemTargetBuildItem outputTargetBuildItem, - ArtifactResultBuildItem lastResult) { + List artifactResultBuildItems) { + ArtifactResultBuildItem lastArtifact = artifactResultBuildItems.get(artifactResultBuildItems.size() - 1); Path quarkusArtifactMetadataPath = outputTargetBuildItem.getOutputDirectory().resolve("quarkus-artifact.properties"); Properties properties = new Properties(); - properties.put("type", lastResult.getType()); - if (lastResult.getPath() != null) { - properties.put("path", outputTargetBuildItem.getOutputDirectory().relativize(lastResult.getPath()).toString()); + properties.put("type", lastArtifact.getType()); + if (lastArtifact.getPath() != null) { + properties.put("path", artifactPathForResultMetadata(outputTargetBuildItem, lastArtifact)); + } else { + if (lastArtifact.getType().endsWith("-container")) { + // in this case we write "path" as to contain the path to the artifact from which the container was built + try { + ArtifactResultBuildItem baseArtifact = artifactResultBuildItems.get(artifactResultBuildItems.size() - 2); + if (baseArtifact.getPath() != null) { + properties.put("path", artifactPathForResultMetadata(outputTargetBuildItem, baseArtifact)); + } + } catch (IndexOutOfBoundsException e) { + // this should never happen really as a container is always built from some other artifact + log.debug(e); + } + } } - Map metadata = lastResult.getMetadata(); + Map metadata = lastArtifact.getMetadata(); if (metadata != null) { for (Map.Entry entry : metadata.entrySet()) { properties.put("metadata." + entry.getKey(), entry.getValue()); @@ -261,6 +274,11 @@ private void writeArtifactResultMetadataFile(BuildSystemTargetBuildItem outputTa } } + private static String artifactPathForResultMetadata(BuildSystemTargetBuildItem outputTargetBuildItem, + ArtifactResultBuildItem artifactResultBuildItem) { + return outputTargetBuildItem.getOutputDirectory().relativize(artifactResultBuildItem.getPath()).toString(); + } + @Override public StartupActionImpl createInitialRuntimeApplication() { if (launchMode == LaunchMode.NORMAL) { diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java index 7809fa7aebe3f..f4b7ab75bf8cb 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java @@ -425,7 +425,16 @@ public static Properties readQuarkusArtifactProperties(ExtensionContext context) } } + static String getEffectiveArtifactType(Properties quarkusArtifactProperties, SmallRyeConfig config) { + Optional maybeType = config.getOptionalValue("quarkus.test.integration-test-artifact-type", String.class); + if (maybeType.isPresent()) { + return maybeType.get(); + } + return getArtifactType(quarkusArtifactProperties); + } + static String getArtifactType(Properties quarkusArtifactProperties) { + String artifactType = quarkusArtifactProperties.getProperty("type"); if (artifactType == null) { throw new IllegalStateException("Unable to determine the type of artifact created by the Quarkus build"); diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTestExtension.java index eadfec79ebee4..c61067576d9c5 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTestExtension.java @@ -8,7 +8,7 @@ import static io.quarkus.test.junit.IntegrationTestUtil.doProcessTestInstance; import static io.quarkus.test.junit.IntegrationTestUtil.ensureNoInjectAnnotationIsUsed; import static io.quarkus.test.junit.IntegrationTestUtil.findProfile; -import static io.quarkus.test.junit.IntegrationTestUtil.getArtifactType; +import static io.quarkus.test.junit.IntegrationTestUtil.getEffectiveArtifactType; import static io.quarkus.test.junit.IntegrationTestUtil.getSysPropsToRestore; import static io.quarkus.test.junit.IntegrationTestUtil.handleDevServices; import static io.quarkus.test.junit.IntegrationTestUtil.readQuarkusArtifactProperties; @@ -190,9 +190,9 @@ private QuarkusTestExtensionState doProcessStart(Properties quarkusArtifactPrope throws Throwable { JBossVersion.disableVersionLogging(); - String artifactType = getArtifactType(quarkusArtifactProperties); - SmallRyeConfig config = ConfigProvider.getConfig().unwrap(SmallRyeConfig.class); + String artifactType = getEffectiveArtifactType(quarkusArtifactProperties, config); + TestConfig testConfig = config.getConfigMapping(TestConfig.class); boolean isDockerLaunch = isContainer(artifactType) || (isJar(artifactType) && "test-with-native-agent".equals(testConfig.integrationTestProfile())); diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusMainIntegrationTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusMainIntegrationTestExtension.java index f1b655f6df85b..68f3b7ad12a29 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusMainIntegrationTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusMainIntegrationTestExtension.java @@ -6,6 +6,7 @@ import static io.quarkus.test.junit.IntegrationTestUtil.determineBuildOutputDirectory; import static io.quarkus.test.junit.IntegrationTestUtil.determineTestProfileAndProperties; import static io.quarkus.test.junit.IntegrationTestUtil.ensureNoInjectAnnotationIsUsed; +import static io.quarkus.test.junit.IntegrationTestUtil.getEffectiveArtifactType; import static io.quarkus.test.junit.IntegrationTestUtil.getSysPropsToRestore; import static io.quarkus.test.junit.IntegrationTestUtil.handleDevServices; import static io.quarkus.test.junit.IntegrationTestUtil.readQuarkusArtifactProperties; @@ -101,11 +102,9 @@ private void prepare(ExtensionContext extensionContext) throws Exception { ensureNoInjectAnnotationIsUsed(testClass, "@QuarkusMainIntegrationTest"); quarkusArtifactProperties = readQuarkusArtifactProperties(extensionContext); - String artifactType = quarkusArtifactProperties.getProperty("type"); - if (artifactType == null) { - throw new IllegalStateException("Unable to determine the type of artifact created by the Quarkus build"); - } SmallRyeConfig config = ConfigProvider.getConfig().unwrap(SmallRyeConfig.class); + String artifactType = getEffectiveArtifactType(quarkusArtifactProperties, config); + TestConfig testConfig = config.getConfigMapping(TestConfig.class); boolean isDockerLaunch = isContainer(artifactType) From a38076586f054075842bcf965e97c2cc44755ada Mon Sep 17 00:00:00 2001 From: Severin Gehwolf Date: Fri, 14 Feb 2025 12:56:34 +0100 Subject: [PATCH 14/16] Fix return type for MariaDB PAM substitution (cherry picked from commit 2a7389f6e35d400c8657668907edfba77cf26814) --- .../runtime/graal/SendPamAuthPacketFactory_Substitutions.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/extensions/jdbc/jdbc-mariadb/runtime/src/main/java/io/quarkus/jdbc/mariadb/runtime/graal/SendPamAuthPacketFactory_Substitutions.java b/extensions/jdbc/jdbc-mariadb/runtime/src/main/java/io/quarkus/jdbc/mariadb/runtime/graal/SendPamAuthPacketFactory_Substitutions.java index 01c32227bd873..6ebfd14e02b35 100644 --- a/extensions/jdbc/jdbc-mariadb/runtime/src/main/java/io/quarkus/jdbc/mariadb/runtime/graal/SendPamAuthPacketFactory_Substitutions.java +++ b/extensions/jdbc/jdbc-mariadb/runtime/src/main/java/io/quarkus/jdbc/mariadb/runtime/graal/SendPamAuthPacketFactory_Substitutions.java @@ -2,6 +2,7 @@ import org.mariadb.jdbc.Configuration; import org.mariadb.jdbc.HostAddress; +import org.mariadb.jdbc.plugin.AuthenticationPlugin; import com.oracle.svm.core.annotate.Substitute; import com.oracle.svm.core.annotate.TargetClass; @@ -10,7 +11,8 @@ public final class SendPamAuthPacketFactory_Substitutions { @Substitute - public void initialize(String authenticationData, byte[] seed, Configuration conf, HostAddress hostAddress) { + public AuthenticationPlugin initialize(String authenticationData, byte[] seed, Configuration conf, + HostAddress hostAddress) { throw new UnsupportedOperationException("Authentication strategy 'dialog' is not supported in GraalVM"); } From f17b3561e751160fad7462ad95c97870eb5e984b Mon Sep 17 00:00:00 2001 From: dc1248 Date: Wed, 12 Feb 2025 16:01:44 +0100 Subject: [PATCH 15/16] Add time zone property for kubernetes and openshift cron jobs (cherry picked from commit c9712fbfd4d8733c9a99e6ff0a5e3d8dba475600) --- .../kubernetes/deployment/AddCronJobResourceDecorator.java | 1 + .../java/io/quarkus/kubernetes/deployment/CronJobConfig.java | 5 +++++ .../src/test/resources/application-kubernetes.properties | 1 + .../it/kubernetes/KubernetesWithCronJobResourceTest.java | 1 + .../it/kubernetes/OpenshiftWithCronJobResourceTest.java | 1 + .../resources/kubernetes-with-cronjob-resource.properties | 1 + .../resources/openshift-with-cronjob-resource.properties | 1 + 7 files changed, 11 insertions(+) diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddCronJobResourceDecorator.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddCronJobResourceDecorator.java index 65ea89d184af6..5152bed1fa7ab 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddCronJobResourceDecorator.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddCronJobResourceDecorator.java @@ -83,6 +83,7 @@ private void initCronJobResourceWithDefaults(CronJobBuilder builder) { config.successfulJobsHistoryLimit().ifPresent(spec::withSuccessfulJobsHistoryLimit); config.failedJobsHistoryLimit().ifPresent(spec::withFailedJobsHistoryLimit); config.startingDeadlineSeconds().ifPresent(spec::withStartingDeadlineSeconds); + config.timeZone().ifPresent(spec::withTimeZone); jobTemplateSpec.withCompletionMode(config.completionMode().name()); jobTemplateSpec.editTemplate().editSpec().withRestartPolicy(config.restartPolicy().name()).endSpec().endTemplate(); diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/CronJobConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/CronJobConfig.java index 5a9a7ed62f6d5..ced9a0208df0d 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/CronJobConfig.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/CronJobConfig.java @@ -13,6 +13,11 @@ public interface CronJobConfig { */ Optional schedule(); + /** + * The time zone for the job schedule. The default value is the local time of the kube-controller-manager. + */ + Optional timeZone(); + /** * ConcurrencyPolicy describes how the job will be handled. */ diff --git a/extensions/kubernetes/vanilla/deployment/src/test/resources/application-kubernetes.properties b/extensions/kubernetes/vanilla/deployment/src/test/resources/application-kubernetes.properties index bdf4d8a914544..d49be42a2c038 100644 --- a/extensions/kubernetes/vanilla/deployment/src/test/resources/application-kubernetes.properties +++ b/extensions/kubernetes/vanilla/deployment/src/test/resources/application-kubernetes.properties @@ -201,6 +201,7 @@ quarkus.kubernetes.job.restart-policy=Never # quarkus.kubernetes.deployment-kind=CronJob quarkus.kubernetes.cron-job.schedule=- +quarkus.kubernetes.cron-job.time-zone=Etc/UTC quarkus.kubernetes.cron-job.concurrency-policy=Forbid quarkus.kubernetes.cron-job.starting-deadline-seconds=7 quarkus.kubernetes.cron-job.failed-jobs-history-limit=7 diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithCronJobResourceTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithCronJobResourceTest.java index 1d7d759dd6230..b8dd597cb6262 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithCronJobResourceTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithCronJobResourceTest.java @@ -50,6 +50,7 @@ public void assertGeneratedResources() throws IOException { }); assertThat(s.getSpec().getSchedule()).isEqualTo("0 0 0 0 *"); + assertThat(s.getSpec().getTimeZone()).isEqualTo("Etc/UTC"); assertThat(s.getSpec().getJobTemplate().getSpec()).satisfies(jobSpec -> { assertThat(jobSpec.getParallelism()).isEqualTo(10); diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithCronJobResourceTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithCronJobResourceTest.java index 3ff7977c2b490..b2188a3025e08 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithCronJobResourceTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithCronJobResourceTest.java @@ -50,6 +50,7 @@ public void assertGeneratedResources() throws IOException { }); assertThat(s.getSpec().getSchedule()).isEqualTo("0 0 0 0 *"); + assertThat(s.getSpec().getTimeZone()).isEqualTo("Etc/UTC"); assertThat(s.getSpec().getJobTemplate().getSpec()).satisfies(jobSpec -> { assertThat(jobSpec.getParallelism()).isEqualTo(10); diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-cronjob-resource.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-cronjob-resource.properties index f812f66973db8..e3de522fe2f34 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-cronjob-resource.properties +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-cronjob-resource.properties @@ -1,5 +1,6 @@ quarkus.kubernetes.deployment-kind=CronJob quarkus.kubernetes.arguments=A,B quarkus.kubernetes.cron-job.schedule=0 0 0 0 * +quarkus.kubernetes.cron-job.time-zone=Etc/UTC quarkus.kubernetes.cron-job.parallelism=10 quarkus.kubernetes.cron-job.restart-policy=Never \ No newline at end of file diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-cronjob-resource.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-cronjob-resource.properties index c8976a0626753..c8dff6958b0a4 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-cronjob-resource.properties +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-cronjob-resource.properties @@ -1,5 +1,6 @@ quarkus.openshift.deployment-kind=CronJob quarkus.openshift.arguments=A,B quarkus.openshift.cron-job.schedule=0 0 0 0 * +quarkus.openshift.cron-job.time-zone=Etc/UTC quarkus.openshift.cron-job.parallelism=10 quarkus.openshift.cron-job.restart-policy=Never \ No newline at end of file From ab23341cea8af74871e6d64c3d2795c5c4d1397e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Fri, 14 Feb 2025 12:46:03 +0100 Subject: [PATCH 16/16] Use UBI9 based Quarkus micro image (cherry picked from commit 5ac3a00ab6f91a84cc93fbad8d54ef8411b34733) --- .../deployment/images/ContainerImages.java | 18 ++++++++++++++---- .../asciidoc/quarkus-runtime-base-image.adoc | 4 ++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/core/deployment/src/main/java/io/quarkus/deployment/images/ContainerImages.java b/core/deployment/src/main/java/io/quarkus/deployment/images/ContainerImages.java index 5abe63b65654c..217c7b738eb34 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/images/ContainerImages.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/images/ContainerImages.java @@ -52,10 +52,20 @@ public class ContainerImages { public static final String UBI9_MINIMAL_VERSION = UBI9_VERSION; public static final String UBI9_MINIMAL = UBI9_MINIMAL_IMAGE_NAME + ":" + UBI9_MINIMAL_VERSION; - // Quarkus Micro image - https://quay.io/repository/quarkus/quarkus-micro-image?tab=tags - public static final String QUARKUS_MICRO_IMAGE_NAME = "quay.io/quarkus/quarkus-micro-image"; - public static final String QUARKUS_MICRO_VERSION = "2.0"; - public static final String QUARKUS_MICRO_IMAGE = QUARKUS_MICRO_IMAGE_NAME + ":" + QUARKUS_MICRO_VERSION; + // UBI 8 Quarkus Micro image - https://quay.io/repository/quarkus/quarkus-micro-image?tab=tags + public static final String UBI8_QUARKUS_MICRO_IMAGE_NAME = "quay.io/quarkus/quarkus-micro-image"; + public static final String UBI8_QUARKUS_MICRO_VERSION = "2.0"; + public static final String UBI8_QUARKUS_MICRO_IMAGE = UBI8_QUARKUS_MICRO_IMAGE_NAME + ":" + UBI8_QUARKUS_MICRO_VERSION; + + // UBI 9 Quarkus Micro image - https://quay.io/repository/quarkus/ubi9-quarkus-micro-image?tab=tags + public static final String UBI9_QUARKUS_MICRO_IMAGE_NAME = "quay.io/quarkus/ubi9-quarkus-micro-image"; + public static final String UBI9_QUARKUS_MICRO_VERSION = "2.0"; + public static final String UBI9_QUARKUS_MICRO_IMAGE = UBI9_QUARKUS_MICRO_IMAGE_NAME + ":" + UBI9_QUARKUS_MICRO_VERSION; + + // default Quarkus Micro image - https://quay.io/repository/quarkus/quarkus-micro-image?tab=tags + public static final String QUARKUS_MICRO_IMAGE_NAME = UBI9_QUARKUS_MICRO_IMAGE_NAME; + public static final String QUARKUS_MICRO_VERSION = UBI9_QUARKUS_MICRO_VERSION; + public static final String QUARKUS_MICRO_IMAGE = UBI9_QUARKUS_MICRO_IMAGE; // === Runtime images for containers (JVM) diff --git a/docs/src/main/asciidoc/quarkus-runtime-base-image.adoc b/docs/src/main/asciidoc/quarkus-runtime-base-image.adoc index d2ebb7802c7db..13f7f2cb03465 100644 --- a/docs/src/main/asciidoc/quarkus-runtime-base-image.adoc +++ b/docs/src/main/asciidoc/quarkus-runtime-base-image.adoc @@ -9,9 +9,9 @@ include::_attributes.adoc[] :topics: docker,podman,images To ease the containerization of native executables, Quarkus provides a base image providing the requirements to run these executables. -The `quarkus-micro-image:2.0` image is: +The `ubi9-quarkus-micro-image:2.0` image is: -* small (based on `ubi8-micro`) +* small (based on `ubi9-micro`) * designed for containers * contains the right set of dependencies (glibc, libstdc++, zlib) * support upx-compressed executables (more details on the xref:upx.adoc[enabling compression documentation])