diff --git a/bom/application/pom.xml b/bom/application/pom.xml
index dbeee6dbdb3e7..7f8a7bc00abae 100644
--- a/bom/application/pom.xml
+++ b/bom/application/pom.xml
@@ -56,7 +56,7 @@
2.0
3.1.1
2.1.0
- 3.3.0
+ 3.3.2
4.0.2
4.0.0
3.4.0
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunner.java
index 15eb300febff0..d826c15c68113 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunner.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunner.java
@@ -28,7 +28,7 @@ public abstract class NativeImageBuildContainerRunner extends NativeImageBuildRu
protected NativeImageBuildContainerRunner(NativeConfig nativeConfig) {
this.nativeConfig = nativeConfig;
- containerRuntime = nativeConfig.containerRuntime().orElseGet(ContainerRuntimeUtil::detectContainerRuntime);
+ containerRuntime = ContainerRuntimeUtil.detectContainerRuntime();
this.baseContainerRuntimeArgs = new String[] { "--env", "LANG=C", "--rm" };
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/UpxCompressionBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/UpxCompressionBuildStep.java
index c58500f1be332..c9275a0c79b7d 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/UpxCompressionBuildStep.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/UpxCompressionBuildStep.java
@@ -104,8 +104,7 @@ private boolean runUpxInContainer(NativeImageBuildItem nativeImage, NativeConfig
List extraArgs = nativeConfig.compression().additionalArgs().orElse(Collections.emptyList());
List commandLine = new ArrayList<>();
- ContainerRuntimeUtil.ContainerRuntime containerRuntime = nativeConfig.containerRuntime()
- .orElseGet(ContainerRuntimeUtil::detectContainerRuntime);
+ ContainerRuntimeUtil.ContainerRuntime containerRuntime = ContainerRuntimeUtil.detectContainerRuntime();
commandLine.add(containerRuntime.getExecutableName());
commandLine.add("run");
diff --git a/core/runtime/src/main/java/io/quarkus/runtime/BuildAnalyticsConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/BuildAnalyticsConfig.java
index e34b1a0e1af54..73929157752bb 100644
--- a/core/runtime/src/main/java/io/quarkus/runtime/BuildAnalyticsConfig.java
+++ b/core/runtime/src/main/java/io/quarkus/runtime/BuildAnalyticsConfig.java
@@ -23,7 +23,7 @@ public class BuildAnalyticsConfig {
/**
* The Segment base URI.
*/
- @ConfigItem
+ @ConfigItem(name = "uri.base")
public Optional uriBase;
/**
diff --git a/core/runtime/src/main/java/io/quarkus/runtime/LaunchConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/LaunchConfig.java
new file mode 100644
index 0000000000000..9e0a6ae50578e
--- /dev/null
+++ b/core/runtime/src/main/java/io/quarkus/runtime/LaunchConfig.java
@@ -0,0 +1,17 @@
+package io.quarkus.runtime;
+
+import io.quarkus.runtime.annotations.ConfigPhase;
+import io.quarkus.runtime.annotations.ConfigRoot;
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithDefault;
+
+@ConfigMapping(prefix = "quarkus.launch")
+@ConfigRoot(phase = ConfigPhase.RUN_TIME)
+public interface LaunchConfig {
+
+ /**
+ * If set to true, Quarkus will perform re-augmentation (assuming the {@code mutable-jar} package type is used)
+ */
+ @WithDefault("false")
+ boolean rebuild();
+}
diff --git a/devtools/gradle/gradle/libs.versions.toml b/devtools/gradle/gradle/libs.versions.toml
index e17526e4c3390..9d42ce3af95b7 100644
--- a/devtools/gradle/gradle/libs.versions.toml
+++ b/devtools/gradle/gradle/libs.versions.toml
@@ -3,7 +3,7 @@ plugin-publish = "1.2.0"
# updating Kotlin here makes QuarkusPluginTest > shouldNotFailOnProjectDependenciesWithoutMain(Path) fail
kotlin = "1.8.10"
-smallrye-config = "3.3.0"
+smallrye-config = "3.3.2"
junit5 = "5.9.3"
assertj = "3.24.2"
diff --git a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java
index 848cd78bedcc9..b06e1b16173dc 100644
--- a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java
+++ b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java
@@ -1061,7 +1061,8 @@ private QuarkusDevModeLauncher newLauncher(Boolean debugPortOk, String bootstrap
}
// Add other properties that may be required for expansion
- for (String value : effectiveProperties.values()) {
+ List effectivePropertyValues = new ArrayList<>(effectiveProperties.values());
+ for (String value : effectivePropertyValues) {
for (String reference : Expression.compile(value, LENIENT_SYNTAX, NO_TRIM).getReferencedStrings()) {
String referenceValue = session.getUserProperties().getProperty(reference);
if (referenceValue != null) {
diff --git a/docs/src/main/asciidoc/datasource.adoc b/docs/src/main/asciidoc/datasource.adoc
index 12fa3b08dd033..8c8f8f6887217 100644
--- a/docs/src/main/asciidoc/datasource.adoc
+++ b/docs/src/main/asciidoc/datasource.adoc
@@ -110,6 +110,7 @@ quarkus.datasource.reactive.max-size=20
----
<1> This configuration value is only required if there is more than one Reactive driver extension on the classpath.
+[[configure-datasources]]
== Configure datasources
The following section describes the configuration for single or multiple datasources.
@@ -431,7 +432,9 @@ You can override this by setting the `transactions` configuration property:
* `quarkus.datasource.jdbc.transactions` for default unnamend datasource
* `quarkus.datasource.__.jdbc.transactions` for named datasource
-See the <> section below.
+For more information, see the <> section below.
+
+To facilitate the storage of transaction logs in a database by using JDBC, see xref:transaction.adoc#jdbcstore[Configuring transaction logs to be stored in a datasource] section of the xref:transaction.adoc[Using transactions in Quarkus] guide.
==== Named datasources
diff --git a/docs/src/main/asciidoc/transaction.adoc b/docs/src/main/asciidoc/transaction.adoc
index bd870f4c95f25..4bbf0f4281e14 100644
--- a/docs/src/main/asciidoc/transaction.adoc
+++ b/docs/src/main/asciidoc/transaction.adoc
@@ -362,15 +362,36 @@ NOTE: The `event` object represents the transaction ID, and defines `toString()`
TIP: In listener methods, you can access more information about the transaction in progress by accessing the `TransactionManager`,
which is a CDI bean and can be ``@Inject``ed.
-== Configuring transaction log to be stored in a DataSource
+[[jdbcstore]]
+== Configure storing of Quarkus transaction logs in a database
-The Narayana project has the capability to store the transaction logs into a JDBC Datasource; this should be our recommendation for users needing transaction recovery capabilities, especially when running in volatile containers.
+In cloud environments where persistent storage is not available, such as when application containers are unable to use persistent volumes, you can configure the transaction management to store transaction logs in a database by using a JDBC datasource.
-To enable this capability, you need to set `quarkus.transaction-manager.object-store.type` to `jdbc` explicitly. Also, you can specify a datasource name to be used for the transaction log storage by setting `quarkus.transaction-manager.object-store.datasource`. It will use the default datasource configuration if not specified.
+IMPORTANT: While there are several benefits to using a database to store transaction logs, you might notice a reduction in performance compared with using the file system to store the logs.
-If you enable `quarkus.transaction-manager.object-store.create-table`, the transaction log table will be created automatically if it does not exist.
+Quarkus allows the following JDBC-specific configuration of the object store included in `quarkus.transacion-manager.object-store.` properties, where can be:
-NOTE: When enabling this capability, the transaction node identifier must be set through `quarkus.transaction-manager.node-name`.
+* `type` (_string_): Configure this property to `jdbc` to enable usage of a Quarkus JDBC datasource for transaction logging.
+The default value is `file-system`.
+* `datasource` (_string_): Specify the name of the datasource for the transaction log storage.
+If no value is provided for the `datasource` property, Quarkus uses the xref:datasource.adoc#configure-datasources[default datasource].
+* `create-table` (_boolean_): When set to `true`, the transaction log table gets automatically created if it does not already exist.
+The default value is `false`.
+* `drop-table` (_boolean_): When set to `true`, the tables are dropped on startup if they already exist.
+The default value is `false`.
+* `table-prefix` (string): Specify the prefix for a related table name.
+The default value is `quarkus_`.
+
+[NOTE]
+====
+To work around the current known issue of link:https://issues.redhat.com/browse/AG-209[Agroal having a different view on running transaction checks], set the datasource transaction type for the datasource responsible for writing the transaction logs to `disabled`:
+
+----
+quarkus.datasource.TX_LOG.jdbc.transactions=disabled
+----
+
+This example uses TX_LOG as the datasource name.
+====
== Why always having a transaction manager?
diff --git a/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java b/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java
index e478e85c7992f..ac7cbb08cd51a 100644
--- a/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java
+++ b/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java
@@ -26,7 +26,7 @@ void testImageWithJava17() {
Path path = getPath("openjdk-17-runtime");
var result = sut.determine(path);
assertThat(result).hasValueSatisfying(v -> {
- assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/openjdk-17-runtime:1.17");
+ assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/openjdk-17-runtime:1.16");
assertThat(v.getJavaVersion()).isEqualTo(17);
});
}
diff --git a/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime b/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime
index d8be4fe7d3662..85a50ddf9aec9 100644
--- a/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime
+++ b/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime
@@ -1,5 +1,5 @@
# Use Java 17 base image
-FROM registry.access.redhat.com/ubi8/openjdk-17-runtime:1.17
+FROM registry.access.redhat.com/ubi8/openjdk-17-runtime:1.16
ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en'
diff --git a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibConfig.java b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibConfig.java
index 9725522370569..106207774b677 100644
--- a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibConfig.java
+++ b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibConfig.java
@@ -16,7 +16,7 @@ public class JibConfig {
/**
* The base image to be used when a container image is being produced for the jar build.
*
- * When the application is built against Java 17 or higher, {@code registry.access.redhat.com/ubi8/openjdk-17-runtime:1.17}
+ * When the application is built against Java 17 or higher, {@code registry.access.redhat.com/ubi8/openjdk-17-runtime:1.16}
* is used as the default.
* Otherwise {@code registry.access.redhat.com/ubi8/openjdk-11-runtime:1.16} is used as the default.
*/
diff --git a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java
index 9eabd8bb6daeb..afa60bb08ea23 100644
--- a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java
+++ b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java
@@ -90,7 +90,7 @@ public class JibProcessor {
private static final IsClassPredicate IS_CLASS_PREDICATE = new IsClassPredicate();
private static final String BINARY_NAME_IN_CONTAINER = "application";
- private static final String JAVA_17_BASE_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17-runtime:1.17";
+ private static final String JAVA_17_BASE_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17-runtime:1.16";
private static final String JAVA_11_BASE_IMAGE = "registry.access.redhat.com/ubi8/openjdk-11-runtime:1.16";
private static final String DEFAULT_BASE_IMAGE_USER = "185";
diff --git a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java
index eef8ad985c913..6c65a0c64e0a2 100644
--- a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java
+++ b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java
@@ -53,7 +53,6 @@
import org.eclipse.microprofile.config.ConfigProvider;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
-import org.xerial.snappy.OSInfo;
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.UnremovableBeanBuildItem;
@@ -95,6 +94,7 @@
import io.quarkus.kafka.client.runtime.KafkaBindingConverter;
import io.quarkus.kafka.client.runtime.KafkaRecorder;
import io.quarkus.kafka.client.runtime.KafkaRuntimeConfigProducer;
+import io.quarkus.kafka.client.runtime.SnappyRecorder;
import io.quarkus.kafka.client.runtime.ui.KafkaTopicClient;
import io.quarkus.kafka.client.runtime.ui.KafkaUiRecorder;
import io.quarkus.kafka.client.runtime.ui.KafkaUiUtils;
@@ -292,7 +292,7 @@ public void build(
}
- @BuildStep(onlyIf = { IsSnappy.class, NativeOrNativeSourcesBuild.class })
+ @BuildStep(onlyIf = { HasSnappy.class, NativeOrNativeSourcesBuild.class })
public void handleSnappyInNative(NativeImageRunnerBuildItem nativeImageRunner,
BuildProducer reflectiveClass,
BuildProducer nativeLibs) {
@@ -307,19 +307,17 @@ public void handleSnappyInNative(NativeImageRunnerBuildItem nativeImageRunner,
String path = root + dir + "/" + snappyNativeLibraryName;
nativeLibs.produce(new NativeImageResourceBuildItem(path));
} else { // otherwise the native lib of the platform this build runs on
- String dir = OSInfo.getNativeLibFolderPathForCurrentOS();
+ String dir = SnappyUtils.getNativeLibFolderPathForCurrentOS();
String snappyNativeLibraryName = System.mapLibraryName("snappyjava");
String path = root + dir + "/" + snappyNativeLibraryName;
nativeLibs.produce(new NativeImageResourceBuildItem(path));
}
}
- @BuildStep
+ @BuildStep(onlyIf = HasSnappy.class)
@Record(ExecutionTime.RUNTIME_INIT)
- void loadSnappyIfEnabled(KafkaRecorder recorder, KafkaBuildTimeConfig config) {
- if (config.snappyEnabled) {
- recorder.loadSnappy();
- }
+ void loadSnappyIfEnabled(SnappyRecorder recorder, KafkaBuildTimeConfig config) {
+ recorder.loadSnappy();
}
@Consume(RuntimeConfigSetupCompleteBuildItem.class)
@@ -585,17 +583,17 @@ public DevConsoleWebjarBuildItem setupWebJar(LaunchModeBuildItem launchModeBuild
.build();
}
- public static final class IsSnappy implements BooleanSupplier {
+ public static final class HasSnappy implements BooleanSupplier {
private final KafkaBuildTimeConfig config;
- public IsSnappy(KafkaBuildTimeConfig config) {
+ public HasSnappy(KafkaBuildTimeConfig config) {
this.config = config;
}
@Override
public boolean getAsBoolean() {
- return config.snappyEnabled;
+ return QuarkusClassLoader.isClassPresentAtRuntime("org.xerial.snappy.OSInfo") && config.snappyEnabled;
}
}
diff --git a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/SnappyUtils.java b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/SnappyUtils.java
new file mode 100644
index 0000000000000..35a9060e72073
--- /dev/null
+++ b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/SnappyUtils.java
@@ -0,0 +1,17 @@
+package io.quarkus.kafka.client.deployment;
+
+import org.xerial.snappy.OSInfo;
+
+/**
+ * This class should only be used if Snappy is available on the classpath.
+ */
+public class SnappyUtils {
+
+ private SnappyUtils() {
+ // Avoid direct instantiation
+ }
+
+ public static String getNativeLibFolderPathForCurrentOS() {
+ return OSInfo.getNativeLibFolderPathForCurrentOS();
+ }
+}
diff --git a/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/KafkaRecorder.java b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/KafkaRecorder.java
index 0de1b980d035b..8520c003c0170 100644
--- a/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/KafkaRecorder.java
+++ b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/KafkaRecorder.java
@@ -1,69 +1,15 @@
package io.quarkus.kafka.client.runtime;
-import java.io.BufferedInputStream;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.net.URL;
import java.util.Optional;
import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.ConfigProvider;
-import org.xerial.snappy.OSInfo;
-import org.xerial.snappy.SnappyError;
-import org.xerial.snappy.SnappyErrorCode;
-import org.xerial.snappy.SnappyLoader;
import io.quarkus.runtime.annotations.Recorder;
@Recorder
public class KafkaRecorder {
- public void loadSnappy() {
- // Resolve the library file name with a suffix (e.g., dll, .so, etc.)
- String snappyNativeLibraryName = System.mapLibraryName("snappyjava");
- String snappyNativeLibraryPath = "/org/xerial/snappy/native/" + OSInfo.getNativeLibFolderPathForCurrentOS();
- boolean hasNativeLib = hasResource(snappyNativeLibraryPath + "/" + snappyNativeLibraryName);
-
- if (!hasNativeLib) {
- String errorMessage = String.format("no native library is found for os.name=%s and os.arch=%s", OSInfo.getOSName(),
- OSInfo.getArchName());
- throw new SnappyError(SnappyErrorCode.FAILED_TO_LOAD_NATIVE_LIBRARY, errorMessage);
- }
-
- File out = extractLibraryFile(
- SnappyLoader.class.getResource(snappyNativeLibraryPath + "/" + snappyNativeLibraryName),
- snappyNativeLibraryName);
-
- System.load(out.getAbsolutePath());
- }
-
- private static boolean hasResource(String path) {
- return SnappyLoader.class.getResource(path) != null;
- }
-
- private static File extractLibraryFile(URL library, String name) {
- String tmp = System.getProperty("java.io.tmpdir");
- File extractedLibFile = new File(tmp, name);
-
- try (BufferedInputStream inputStream = new BufferedInputStream(library.openStream());
- FileOutputStream fileOS = new FileOutputStream(extractedLibFile)) {
- byte[] data = new byte[8192];
- int byteContent;
- while ((byteContent = inputStream.read(data, 0, 8192)) != -1) {
- fileOS.write(data, 0, byteContent);
- }
- } catch (IOException e) {
- throw new UncheckedIOException(
- "Unable to extract native library " + name + " to " + extractedLibFile.getAbsolutePath(), e);
- }
-
- extractedLibFile.deleteOnExit();
-
- return extractedLibFile;
- }
-
public void checkBoostrapServers() {
Config config = ConfigProvider.getConfig();
Boolean serviceBindingEnabled = config.getValue("quarkus.kubernetes-service-binding.enabled", Boolean.class);
diff --git a/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/SnappyRecorder.java b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/SnappyRecorder.java
new file mode 100644
index 0000000000000..e06726204230e
--- /dev/null
+++ b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/SnappyRecorder.java
@@ -0,0 +1,61 @@
+package io.quarkus.kafka.client.runtime;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.net.URL;
+
+import org.xerial.snappy.OSInfo;
+import org.xerial.snappy.SnappyLoader;
+
+import io.quarkus.runtime.annotations.Recorder;
+
+@Recorder
+public class SnappyRecorder {
+
+ public void loadSnappy() {
+ // Resolve the library file name with a suffix (e.g., dll, .so, etc.)
+ String snappyNativeLibraryName = System.mapLibraryName("snappyjava");
+ String snappyNativeLibraryPath = "/org/xerial/snappy/native/" + OSInfo.getNativeLibFolderPathForCurrentOS();
+ boolean hasNativeLib = hasResource(snappyNativeLibraryPath + "/" + snappyNativeLibraryName);
+
+ if (!hasNativeLib) {
+ String errorMessage = String.format("no native library is found for os.name=%s and os.arch=%s", OSInfo.getOSName(),
+ OSInfo.getArchName());
+ throw new RuntimeException(errorMessage);
+ }
+
+ File out = extractLibraryFile(
+ SnappyLoader.class.getResource(snappyNativeLibraryPath + "/" + snappyNativeLibraryName),
+ snappyNativeLibraryName);
+
+ System.load(out.getAbsolutePath());
+ }
+
+ private static boolean hasResource(String path) {
+ return SnappyLoader.class.getResource(path) != null;
+ }
+
+ private static File extractLibraryFile(URL library, String name) {
+ String tmp = System.getProperty("java.io.tmpdir");
+ File extractedLibFile = new File(tmp, name);
+
+ try (BufferedInputStream inputStream = new BufferedInputStream(library.openStream());
+ FileOutputStream fileOS = new FileOutputStream(extractedLibFile)) {
+ byte[] data = new byte[8192];
+ int byteContent;
+ while ((byteContent = inputStream.read(data, 0, 8192)) != -1) {
+ fileOS.write(data, 0, byteContent);
+ }
+ } catch (IOException e) {
+ throw new UncheckedIOException(
+ "Unable to extract native library " + name + " to " + extractedLibFile.getAbsolutePath(), e);
+ }
+
+ extractedLibFile.deleteOnExit();
+
+ return extractedLibFile;
+ }
+}
diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java
index 35dcae0e411df..b9340ab472239 100644
--- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java
+++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java
@@ -859,6 +859,30 @@ public enum ResponseMode {
@ConfigItem(defaultValue = "true")
public boolean allowMultipleCodeFlows = true;
+ /**
+ * Fail with the HTTP 401 error if the state cookie is present but no state query parameter is present.
+ *
+ * When either multiple authentications are disabled or the redirect URL
+ * matches the original request URL, the stale state cookie might remain in the browser cache from
+ * the earlier failed redirect to an OpenId Connect provider and be visible during the current request.
+ * For example, if Single-page application (SPA) uses XHR to handle redirects to the provider
+ * which does not support CORS for its authorization endpoint, the browser will block it
+ * and the state cookie created by Quarkus will remain in the browser cache.
+ * Quarkus will report an authentication failure when it will detect such an old state cookie but find no matching state
+ * query parameter.
+ *
+ * Reporting HTTP 401 error is usually the right thing to do in such cases, it will minimize a risk of the
+ * browser redirect loop but also can identify problems in the way SPA or Quarkus application manage redirects.
+ * For example, enabling {@link #javaScriptAutoRedirect} or having the provider redirect to URL configured
+ * with {@link #redirectPath} may be needed to avoid such errors.
+ *
+ * However, setting this property to `false` may help if the above options are not suitable.
+ * It will cause a new authentication redirect to OpenId Connect provider. Please be aware doing so may increase the
+ * risk of browser redirect loops.
+ */
+ @ConfigItem(defaultValue = "true")
+ public boolean failOnMissingStateParam = true;
+
/**
* If this property is set to 'true' then an OIDC UserInfo endpoint will be called.
* This property will be enabled if `quarkus.oidc.roles.source` is `userinfo`
diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java
index 0104dc8dc68ed..33a546e4b2e3c 100644
--- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java
+++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java
@@ -106,7 +106,7 @@ public Uni apply(TenantConfigContext tenantContext) {
@Override
public Uni extends SecurityIdentity> apply(MultiMap requestParams) {
return processRedirectFromOidc(context, oidcTenantConfig, identityProviderManager,
- requestParams);
+ requestParams, cookies);
}
});
}
@@ -115,7 +115,7 @@ public Uni extends SecurityIdentity> apply(MultiMap requestParams) {
return Uni.createFrom().failure(new AuthenticationFailedException());
} else {
return processRedirectFromOidc(context, oidcTenantConfig, identityProviderManager,
- context.queryParams());
+ context.queryParams(), cookies);
}
}
@@ -136,7 +136,8 @@ private boolean isStateCookieAvailable(Map cookies) {
}
private Uni processRedirectFromOidc(RoutingContext context, OidcTenantConfig oidcTenantConfig,
- IdentityProviderManager identityProviderManager, MultiMap requestParams) {
+ IdentityProviderManager identityProviderManager, MultiMap requestParams,
+ Map cookies) {
// At this point it has already been detected that some state cookie is available.
// If the state query parameter is not available or is available but no matching state cookie is found then if
@@ -150,16 +151,14 @@ private Uni processRedirectFromOidc(RoutingContext context, Oi
List stateQueryParam = requestParams.getAll(OidcConstants.CODE_FLOW_STATE);
if (stateQueryParam.size() != 1) {
- LOG.debug("State parameter can not be empty or multi-valued if the state cookie is present");
- return stateCookieIsMissing(oidcTenantConfig, context);
+ return stateParamIsMissing(oidcTenantConfig, context, cookies, stateQueryParam.size() > 1);
}
final Cookie stateCookie = context.request().getCookie(
getStateCookieName(oidcTenantConfig) + "_" + stateQueryParam.get(0));
if (stateCookie == null) {
- LOG.debug("Matching state cookie is not found");
- return stateCookieIsMissing(oidcTenantConfig, context);
+ return stateCookieIsMissing(oidcTenantConfig, context, cookies);
}
String[] parsedStateCookieValue = COOKIE_PATTERN.split(stateCookie.getValue());
@@ -239,14 +238,44 @@ public Uni apply(TenantConfigContext tenantContext) {
}
- private Uni stateCookieIsMissing(OidcTenantConfig oidcTenantConfig, RoutingContext context) {
+ private Uni stateParamIsMissing(OidcTenantConfig oidcTenantConfig, RoutingContext context,
+ Map cookies, boolean multipleStateQueryParams) {
+ if (multipleStateQueryParams) {
+ LOG.warn("State query parameter can not be multi-valued if the state cookie is present");
+ removeStateCookies(oidcTenantConfig, context, cookies);
+ return Uni.createFrom().failure(new AuthenticationCompletionException());
+ }
+ LOG.debug("State parameter can not be empty if the state cookie is present");
+ return stateCookieIsNotMatched(oidcTenantConfig, context, cookies);
+ }
+
+ private Uni stateCookieIsMissing(OidcTenantConfig oidcTenantConfig, RoutingContext context,
+ Map cookies) {
+ LOG.debug("Matching state cookie is not found");
+ return stateCookieIsNotMatched(oidcTenantConfig, context, cookies);
+ }
+
+ private Uni stateCookieIsNotMatched(OidcTenantConfig oidcTenantConfig, RoutingContext context,
+ Map cookies) {
if (!oidcTenantConfig.authentication.allowMultipleCodeFlows
|| context.request().path().equals(getRedirectPath(oidcTenantConfig, context))) {
- return Uni.createFrom().failure(new AuthenticationCompletionException());
- } else {
- context.put(NO_OIDC_COOKIES_AVAILABLE, Boolean.TRUE);
- return Uni.createFrom().optional(Optional.empty());
+ if (oidcTenantConfig.authentication.failOnMissingStateParam) {
+ return Uni.createFrom().failure(new AuthenticationCompletionException());
+ } else {
+ removeStateCookies(oidcTenantConfig, context, cookies);
+ }
+ }
+ context.put(NO_OIDC_COOKIES_AVAILABLE, Boolean.TRUE);
+ return Uni.createFrom().optional(Optional.empty());
+ }
+
+ private void removeStateCookies(OidcTenantConfig oidcTenantConfig, RoutingContext context, Map cookies) {
+ for (String name : cookies.keySet()) {
+ if (name.startsWith(OidcUtils.STATE_COOKIE_NAME)) {
+ OidcUtils.removeCookie(context, oidcTenantConfig, name);
+ }
}
+
}
private String getRequestParametersAsQuery(URI requestUri, MultiMap requestParams, OidcTenantConfig oidcConfig) {
diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/ClassRoutingHandler.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/ClassRoutingHandler.java
index 96021b564068d..df1d6de283c35 100644
--- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/ClassRoutingHandler.java
+++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/ClassRoutingHandler.java
@@ -127,10 +127,14 @@ public void handle(ResteasyReactiveRequestContext requestContext) throws Excepti
if (!accepts.isEmpty()) {
boolean hasAtLeastOneMatch = false;
for (int i = 0; i < accepts.size(); i++) {
- boolean matches = acceptHeaderMatches(target, accepts.get(i));
- if (matches) {
- hasAtLeastOneMatch = true;
- break;
+ try {
+ boolean matches = acceptHeaderMatches(target, accepts.get(i));
+ if (matches) {
+ hasAtLeastOneMatch = true;
+ break;
+ }
+ } catch (IllegalArgumentException ignored) {
+ // the provided header was not valid
}
}
if (!hasAtLeastOneMatch) {
@@ -150,6 +154,10 @@ public void handle(ResteasyReactiveRequestContext requestContext) throws Excepti
}
}
+ /**
+ * @return {@code true} if the provided string matches one of the {@code @Produces} values of the resource method
+ * @throws IllegalArgumentException if the provided string cannot be parsed into a {@link MediaType}
+ */
private boolean acceptHeaderMatches(RequestMapper.RequestMatch target, String accepts) {
if ((accepts != null) && !accepts.equals(MediaType.WILDCARD)) {
int commaIndex = accepts.indexOf(',');
@@ -157,9 +165,8 @@ private boolean acceptHeaderMatches(RequestMapper.RequestMatch
MediaType[] producesMediaTypes = target.value.getProduces().getSortedOriginalMediaTypes();
if (!multipleAcceptsValues && (producesMediaTypes.length == 1)) {
// the point of this branch is to eliminate any list creation or string indexing as none is needed
- MediaType acceptsMediaType = MediaType.valueOf(accepts.trim());
MediaType providedMediaType = producesMediaTypes[0];
- return providedMediaType.isCompatible(acceptsMediaType);
+ return providedMediaType.isCompatible(toMediaType(accepts.trim()));
} else if (multipleAcceptsValues && (producesMediaTypes.length == 1)) {
// this is fairly common case, so we want it to be as fast as possible
// we do that by manually splitting the accepts header and immediately checking
diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/mediatype/InvalidAcceptTest.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/mediatype/InvalidAcceptTest.java
new file mode 100644
index 0000000000000..507e8c4d6bfa9
--- /dev/null
+++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/mediatype/InvalidAcceptTest.java
@@ -0,0 +1,50 @@
+package org.jboss.resteasy.reactive.server.vertx.test.mediatype;
+
+import static io.restassured.RestAssured.config;
+import static io.restassured.RestAssured.given;
+import static io.restassured.config.EncoderConfig.encoderConfig;
+
+import java.util.function.Supplier;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+
+import org.jboss.resteasy.reactive.server.vertx.test.framework.ResteasyReactiveUnitTest;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.restassured.http.ContentType;
+
+public class InvalidAcceptTest {
+
+ @RegisterExtension
+ static ResteasyReactiveUnitTest test = new ResteasyReactiveUnitTest()
+ .setArchiveProducer(new Supplier<>() {
+ @Override
+ public JavaArchive get() {
+ return ShrinkWrap.create(JavaArchive.class)
+ .addClasses(HelloResource.class);
+ }
+ });
+
+ @Test
+ public void test() {
+ given().config(config().encoderConfig(encoderConfig().encodeContentTypeAs("invalid", ContentType.TEXT))).body("dummy")
+ .accept("invalid").get("/hello")
+ .then()
+ .statusCode(406);
+ }
+
+ @Path("hello")
+ public static class HelloResource {
+
+ @Produces("text/plain")
+ @GET
+ public String hello() {
+ return "hello";
+ }
+ }
+}
diff --git a/integration-tests/oidc-code-flow/src/main/resources/application.properties b/integration-tests/oidc-code-flow/src/main/resources/application.properties
index ef7d0fa28bd28..2f7bfe6302863 100644
--- a/integration-tests/oidc-code-flow/src/main/resources/application.properties
+++ b/integration-tests/oidc-code-flow/src/main/resources/application.properties
@@ -109,6 +109,7 @@ quarkus.oidc.tenant-https.authentication.error-path=/tenant-https/error
quarkus.oidc.tenant-https.authentication.pkce-required=true
quarkus.oidc.tenant-https.authentication.pkce-secret=eUk1p7UB3nFiXZGUXi0uph1Y9p34YhBU
quarkus.oidc.tenant-https.authentication.cookie-same-site=strict
+quarkus.oidc.tenant-https.authentication.fail-on-missing-state-param=false
quarkus.oidc.tenant-javascript.auth-server-url=${quarkus.oidc.auth-server-url}
quarkus.oidc.tenant-javascript.client-id=quarkus-app
diff --git a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java
index 085b1ac532f88..40e2da377d0d0 100644
--- a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java
+++ b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java
@@ -227,6 +227,56 @@ public void testCodeFlowForceHttpsRedirectUriAndPkce() throws Exception {
}
}
+ @Test
+ public void testStateCookieIsPresentButStateParamNot() throws Exception {
+ try (final WebClient webClient = createWebClient()) {
+ webClient.getOptions().setRedirectEnabled(false);
+
+ WebResponse webResponse = webClient
+ .loadWebResponse(
+ new WebRequest(URI.create("http://localhost:8081/tenant-https").toURL()));
+ String keycloakUrl = webResponse.getResponseHeaderValue("location");
+ verifyLocationHeader(webClient, keycloakUrl, "tenant-https_test", "tenant-https",
+ true);
+
+ HtmlPage page = webClient.getPage(keycloakUrl);
+
+ assertEquals("Sign in to quarkus", page.getTitleText());
+ HtmlForm loginForm = page.getForms().get(0);
+ loginForm.getInputByName("username").setValueAttribute("alice");
+ loginForm.getInputByName("password").setValueAttribute("alice");
+
+ webClient.getOptions().setThrowExceptionOnFailingStatusCode(false);
+ webResponse = loginForm.getInputByName("login").click().getWebResponse();
+ webClient.getOptions().setThrowExceptionOnFailingStatusCode(true);
+
+ // This is a redirect from the OIDC server to the endpoint containing the state and code
+ String endpointLocation = webResponse.getResponseHeaderValue("location");
+ assertTrue(endpointLocation.startsWith("https"));
+ endpointLocation = "http" + endpointLocation.substring(5);
+
+ // State cookie is present
+ Cookie stateCookie = getStateCookie(webClient, "tenant-https_test");
+ assertNull(stateCookie.getSameSite());
+ verifyCodeVerifier(stateCookie, keycloakUrl);
+
+ // Make a call without an extra state query param, status is 401
+ webResponse = webClient.loadWebResponse(new WebRequest(URI.create(endpointLocation + "&state=123").toURL()));
+ assertEquals(401, webResponse.getStatusCode());
+
+ // Make a call without the state query param, confirm the old state cookie is removed, status is 302
+ webResponse = webClient.loadWebResponse(new WebRequest(URI.create("http://localhost:8081/tenant-https").toURL()));
+ assertEquals(302, webResponse.getStatusCode());
+ // the old state cookie has been removed
+ assertNull(webClient.getCookieManager().getCookie(stateCookie.getName()));
+ // new state cookie is created
+ Cookie newStateCookie = getStateCookie(webClient, "tenant-https_test");
+ assertNotEquals(newStateCookie.getName(), stateCookie.getName());
+
+ webClient.getCookieManager().clearCookies();
+ }
+ }
+
@Test
public void testCodeFlowForceHttpsRedirectUriWithQueryAndPkce() throws Exception {
try (final WebClient webClient = createWebClient()) {