diff --git a/bom/application/pom.xml b/bom/application/pom.xml
index 83a4d773d47546..06bf1b8c876323 100644
--- a/bom/application/pom.xml
+++ b/bom/application/pom.xml
@@ -878,6 +878,16 @@
quarkus-oidc-token-propagation-deployment
${project.version}
+
+ io.quarkus
+ quarkus-oidc-db-token-state-manager
+ ${project.version}
+
+
+ io.quarkus
+ quarkus-oidc-db-token-state-manager-deployment
+ ${project.version}
+
io.quarkus
quarkus-oidc-token-propagation-reactive
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/Capability.java b/core/deployment/src/main/java/io/quarkus/deployment/Capability.java
index 4d0fd6495051ce..5f3d5d46246d1f 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/Capability.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/Capability.java
@@ -150,4 +150,9 @@ public interface Capability {
String CACHE = QUARKUS_PREFIX + ".cache";
String JDBC_ORACLE = QUARKUS_PREFIX + ".jdbc.oracle";
+ String REACTIVE_PG_CLIENT = QUARKUS_PREFIX + ".reactive-pg-client";
+ String REACTIVE_ORACLE_CLIENT = QUARKUS_PREFIX + ".reactive-oracle-client";
+ String REACTIVE_MYSQL_CLIENT = QUARKUS_PREFIX + ".reactive-mysql-client";
+ String REACTIVE_MSSQL_CLIENT = QUARKUS_PREFIX + ".reactive-mssql-client";
+ String REACTIVE_DB2_CLIENT = QUARKUS_PREFIX + ".reactive-db2-client";
}
diff --git a/devtools/bom-descriptor-json/pom.xml b/devtools/bom-descriptor-json/pom.xml
index 230bd9f29e5e79..22c2cca510f18d 100644
--- a/devtools/bom-descriptor-json/pom.xml
+++ b/devtools/bom-descriptor-json/pom.xml
@@ -1591,6 +1591,19 @@
+
+ io.quarkus
+ quarkus-oidc-db-token-state-manager
+ ${project.version}
+ pom
+ test
+
+
+ *
+ *
+
+
+
io.quarkus
quarkus-oidc-token-propagation
diff --git a/docs/pom.xml b/docs/pom.xml
index 5d8fd796d6c594..d28aa46df1760d 100644
--- a/docs/pom.xml
+++ b/docs/pom.xml
@@ -1607,6 +1607,19 @@
+
+ io.quarkus
+ quarkus-oidc-db-token-state-manager-deployment
+ ${project.version}
+ pom
+ test
+
+
+ *
+ *
+
+
+
io.quarkus
quarkus-oidc-token-propagation-deployment
diff --git a/extensions/oidc-db-token-state-manager/deployment/pom.xml b/extensions/oidc-db-token-state-manager/deployment/pom.xml
new file mode 100644
index 00000000000000..f05a8b5b250fca
--- /dev/null
+++ b/extensions/oidc-db-token-state-manager/deployment/pom.xml
@@ -0,0 +1,99 @@
+
+
+
+ quarkus-oidc-db-token-state-manager-parent
+ io.quarkus
+ 999-SNAPSHOT
+ ../
+
+ 4.0.0
+
+ quarkus-oidc-db-token-state-manager-deployment
+ Quarkus - OpenID Connect Database Token State Manager - Deployment
+
+
+
+ io.quarkus
+ quarkus-oidc-db-token-state-manager
+
+
+ io.quarkus
+ quarkus-oidc-deployment
+
+
+
+ io.quarkus
+ quarkus-resteasy-reactive-deployment
+ test
+
+
+ io.quarkus
+ quarkus-hibernate-orm-deployment
+ test
+
+
+ io.quarkus
+ quarkus-junit5-internal
+ test
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+ net.sourceforge.htmlunit
+ htmlunit
+ test
+
+
+
+
+
+
+ maven-compiler-plugin
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ ${project.version}
+
+
+
+
+
+ maven-surefire-plugin
+
+ true
+
+
+
+
+
+
+ test-keycloak
+
+
+ test-containers
+
+
+
+
+
+ maven-surefire-plugin
+
+ false
+
+ ${mssql.image}
+ ${db2.image}
+
+
+
+
+
+
+
+
diff --git a/extensions/oidc-db-token-state-manager/deployment/src/main/java/io/quarkus/oidc/db/token/state/manager/OidcDbTokenStateManagerBuildTimeConfig.java b/extensions/oidc-db-token-state-manager/deployment/src/main/java/io/quarkus/oidc/db/token/state/manager/OidcDbTokenStateManagerBuildTimeConfig.java
new file mode 100644
index 00000000000000..a6df4aeb4cecca
--- /dev/null
+++ b/extensions/oidc-db-token-state-manager/deployment/src/main/java/io/quarkus/oidc/db/token/state/manager/OidcDbTokenStateManagerBuildTimeConfig.java
@@ -0,0 +1,17 @@
+package io.quarkus.oidc.db.token.state.manager;
+
+import io.quarkus.runtime.annotations.ConfigRoot;
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithDefault;
+
+@ConfigMapping(prefix = "quarkus.oidc-db-token-state-manager")
+@ConfigRoot
+public interface OidcDbTokenStateManagerBuildTimeConfig {
+
+ /**
+ * Whether token state should be stored in the database.
+ */
+ @WithDefault("true")
+ boolean enabled();
+
+}
diff --git a/extensions/oidc-db-token-state-manager/deployment/src/main/java/io/quarkus/oidc/db/token/state/manager/OidcDbTokenStateManagerProcessor.java b/extensions/oidc-db-token-state-manager/deployment/src/main/java/io/quarkus/oidc/db/token/state/manager/OidcDbTokenStateManagerProcessor.java
new file mode 100644
index 00000000000000..21fbab32ec4786
--- /dev/null
+++ b/extensions/oidc-db-token-state-manager/deployment/src/main/java/io/quarkus/oidc/db/token/state/manager/OidcDbTokenStateManagerProcessor.java
@@ -0,0 +1,213 @@
+package io.quarkus.oidc.db.token.state.manager;
+
+import static io.quarkus.deployment.Capability.REACTIVE_DB2_CLIENT;
+import static io.quarkus.deployment.Capability.REACTIVE_MSSQL_CLIENT;
+import static io.quarkus.deployment.Capability.REACTIVE_MYSQL_CLIENT;
+import static io.quarkus.deployment.Capability.REACTIVE_ORACLE_CLIENT;
+import static io.quarkus.deployment.Capability.REACTIVE_PG_CLIENT;
+import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT;
+import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT;
+
+import java.util.function.BooleanSupplier;
+
+import jakarta.enterprise.context.Dependent;
+import jakarta.inject.Singleton;
+
+import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
+import io.quarkus.arc.deployment.BeanContainerBuildItem;
+import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
+import io.quarkus.arc.deployment.SyntheticBeansRuntimeInitBuildItem;
+import io.quarkus.arc.deployment.ValidationPhaseBuildItem;
+import io.quarkus.builder.item.SimpleBuildItem;
+import io.quarkus.deployment.Capabilities;
+import io.quarkus.deployment.annotations.BuildProducer;
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.annotations.BuildSteps;
+import io.quarkus.deployment.annotations.Consume;
+import io.quarkus.deployment.annotations.Record;
+import io.quarkus.deployment.builditem.RuntimeConfigSetupCompleteBuildItem;
+import io.quarkus.oidc.TokenStateManager;
+import io.quarkus.oidc.db.token.state.manager.runtime.OidcDbTokenStateManager;
+import io.quarkus.oidc.db.token.state.manager.runtime.OidcDbTokenStateManagerInitializer;
+import io.quarkus.oidc.db.token.state.manager.runtime.OidcDbTokenStateManagerInitializer.OidcDbTokenStateManagerInitializerProperties;
+import io.quarkus.oidc.db.token.state.manager.runtime.OidcDbTokenStateManagerRecorder;
+import io.quarkus.runtime.configuration.ConfigurationException;
+
+@BuildSteps(onlyIf = OidcDbTokenStateManagerProcessor.OidcDbTokenStateManagerEnabled.class)
+public class OidcDbTokenStateManagerProcessor {
+
+ private static final String[] SUPPORTED_REACTIVE_CLIENTS = new String[] { REACTIVE_PG_CLIENT, REACTIVE_MYSQL_CLIENT,
+ REACTIVE_MSSQL_CLIENT, REACTIVE_DB2_CLIENT, REACTIVE_ORACLE_CLIENT };
+
+ @Record(STATIC_INIT)
+ @BuildStep
+ SyntheticBeanBuildItem produceDbTokenStateManagerBean(OidcDbTokenStateManagerRecorder recorder,
+ ReactiveSqlClientBuildItem sqlClientBuildItem) {
+ boolean isMySQL = false;
+ final String insertStatement;
+ switch (sqlClientBuildItem.reactiveClient) {
+ case REACTIVE_PG_CLIENT:
+ insertStatement = "INSERT INTO oidc_db_token_state_manager (id_token, access_token, refresh_token, expires_in) VALUES ($1, $2, $3, $4) RETURNING id";
+ break;
+ case REACTIVE_MYSQL_CLIENT:
+ isMySQL = true;
+ insertStatement = "INSERT INTO oidc_db_token_state_manager (id_token, access_token, refresh_token, expires_in) VALUES (?, ?, ?, ?)";
+ break;
+ case REACTIVE_MSSQL_CLIENT:
+ insertStatement = "INSERT INTO oidc_db_token_state_manager (id_token, access_token, refresh_token, expires_in) OUTPUT INSERTED.id VALUES (@p1, @p2, @p3, @p4)";
+ break;
+ case REACTIVE_DB2_CLIENT:
+ insertStatement = "SELECT id FROM FINAL TABLE (INSERT INTO oidc_db_token_state_manager (id_token, access_token, refresh_token, expires_in) VALUES (?, ?, ?, ?))";
+ break;
+ case REACTIVE_ORACLE_CLIENT:
+ // FIXME: make this optimal with io.vertx.oracleclient.OraclePrepareOptions.setAutoGeneratedKeysIndexes
+ // once on Oracle 24.x driver (as 23.4.0.0 will probably not be available for community)
+ // see https://github.com/eclipse-vertx/vertx-sql-client/issues/1343 for more info
+ insertStatement = "SELECT INSERT_TOKENS(?, ?, ?, ?) FROM DUAL";
+ break;
+ default:
+ throw new RuntimeException("Unknown Reactive Sql Client " + sqlClientBuildItem.reactiveClient);
+ }
+ return SyntheticBeanBuildItem
+ .configure(OidcDbTokenStateManager.class)
+ .alternative(true)
+ .priority(1)
+ .addType(TokenStateManager.class)
+ .unremovable()
+ .scope(Singleton.class)
+ .supplier(recorder.createTokenStateManager(insertStatement, isMySQL))
+ .done();
+ }
+
+ @BuildStep
+ ReactiveSqlClientBuildItem validateReactiveSqlClient(
+ BuildProducer validationErrors,
+ Capabilities capabilities) {
+ ReactiveSqlClientBuildItem sqlClientDbTable = null;
+ for (String reactiveClient : SUPPORTED_REACTIVE_CLIENTS) {
+ if (capabilities.isPresent(reactiveClient)) {
+ if (sqlClientDbTable == null) {
+ sqlClientDbTable = new ReactiveSqlClientBuildItem(reactiveClient);
+ } else {
+ validationErrors.produce(new ValidationPhaseBuildItem.ValidationErrorBuildItem(
+ new ConfigurationException("The OpenID Connect Database Token State Manager extension is "
+ + "only supported when exactly one Reactive SQL Client extension is present.")));
+ return null;
+ }
+ }
+ }
+ if (sqlClientDbTable == null) {
+ validationErrors.produce(new ValidationPhaseBuildItem.ValidationErrorBuildItem(new ConfigurationException(
+ "The OpenID Connect Database Token State Manager extension requires Reactive SQL Client extension. "
+ + "Please refer to the https://quarkus.io/guides/reactive-sql-clients for more information.")));
+ }
+ return sqlClientDbTable;
+ }
+
+ @BuildStep
+ AdditionalBeanBuildItem makeDbTokenStateManagerInitializerBean() {
+ return new AdditionalBeanBuildItem(OidcDbTokenStateManagerInitializer.class);
+ }
+
+ @BuildStep
+ @Record(STATIC_INIT)
+ SyntheticBeanBuildItem createDbTokenStateInitializerProps(ReactiveSqlClientBuildItem sqlClientBuildItem,
+ OidcDbTokenStateManagerRecorder recorder) {
+ final String createTableDdl;
+ final boolean supportsIfTableNotExists;
+ boolean isOracleClient = false;
+ switch (sqlClientBuildItem.reactiveClient) {
+ case REACTIVE_PG_CLIENT:
+ createTableDdl = "CREATE TABLE IF NOT EXISTS oidc_db_token_state_manager (" +
+ "id BIGSERIAL PRIMARY KEY, " +
+ "id_token VARCHAR, " +
+ "access_token VARCHAR, " +
+ "expires_in BIGINT NOT NULL, " +
+ "refresh_token VARCHAR)";
+ supportsIfTableNotExists = true;
+ break;
+ case REACTIVE_MYSQL_CLIENT:
+ createTableDdl = "CREATE TABLE IF NOT EXISTS oidc_db_token_state_manager ("
+ + "id BIGINT NOT NULL AUTO_INCREMENT, "
+ + "id_token VARCHAR(5000) NULL, "
+ + "access_token VARCHAR(5000) NULL, "
+ + "refresh_token VARCHAR(5000) NULL, "
+ + "expires_in BIGINT NOT NULL, "
+ + "PRIMARY KEY (id))";
+ supportsIfTableNotExists = true;
+ break;
+ case REACTIVE_MSSQL_CLIENT:
+ createTableDdl = "CREATE TABLE oidc_db_token_state_manager ("
+ + "id BIGINT IDENTITY(1,1) PRIMARY KEY, "
+ + "id_token NVARCHAR(MAX), "
+ + "access_token NVARCHAR(MAX), "
+ + "refresh_token NVARCHAR(MAX), "
+ + "expires_in BIGINT NOT NULL)";
+ supportsIfTableNotExists = false;
+ break;
+ case REACTIVE_DB2_CLIENT:
+ createTableDdl = "CREATE TABLE oidc_db_token_state_manager ("
+ + "id BIGINT NOT NULL GENERATED ALWAYS AS IDENTITY (START WITH 1 INCREMENT BY 1), "
+ + "id_token VARCHAR(4000), "
+ + "access_token VARCHAR(4000), "
+ + "refresh_token VARCHAR(4000), "
+ + "expires_in BIGINT NOT NULL, "
+ + "PRIMARY KEY (id))";
+ supportsIfTableNotExists = false;
+ break;
+ case REACTIVE_ORACLE_CLIENT:
+ createTableDdl = "CREATE TABLE IF NOT EXISTS oidc_db_token_state_manager ("
+ + "id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, "
+ + "id_token VARCHAR2(4000), "
+ + "access_token VARCHAR2(4000), "
+ + "refresh_token VARCHAR2(4000), "
+ + "expires_in NUMBER NOT NULL)";
+ supportsIfTableNotExists = true;
+ isOracleClient = true;
+ break;
+ default:
+ throw new ConfigurationException("Unknown Reactive Sql Client " + sqlClientBuildItem.reactiveClient);
+ }
+ return SyntheticBeanBuildItem
+ .configure(OidcDbTokenStateManagerInitializerProperties.class)
+ .supplier(recorder.createDbTokenStateInitializerProps(createTableDdl, supportsIfTableNotExists,
+ isOracleClient))
+ .unremovable()
+ .scope(Dependent.class)
+ .done();
+ }
+
+ @Consume(RuntimeConfigSetupCompleteBuildItem.class)
+ @Record(RUNTIME_INIT)
+ @BuildStep
+ void validateTenantConfig(OidcDbTokenStateManagerRecorder recorder) {
+ recorder.validateOidcTenantConfig();
+ }
+
+ @Consume(SyntheticBeansRuntimeInitBuildItem.class)
+ @Record(RUNTIME_INIT)
+ @BuildStep
+ void setSqlClientPool(OidcDbTokenStateManagerRecorder recorder, BeanContainerBuildItem beanContainer) {
+ recorder.setSqlClientPool(beanContainer.getValue());
+ }
+
+ static final class OidcDbTokenStateManagerEnabled implements BooleanSupplier {
+
+ OidcDbTokenStateManagerBuildTimeConfig config;
+
+ @Override
+ public boolean getAsBoolean() {
+ return config.enabled();
+ }
+ }
+
+ static final class ReactiveSqlClientBuildItem extends SimpleBuildItem {
+
+ private final String reactiveClient;
+
+ private ReactiveSqlClientBuildItem(String reactiveClient) {
+ this.reactiveClient = reactiveClient;
+ }
+ }
+
+}
diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/AbstractDbTokenStateManagerTest.java b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/AbstractDbTokenStateManagerTest.java
new file mode 100644
index 00000000000000..e506b2f63d4fcb
--- /dev/null
+++ b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/AbstractDbTokenStateManagerTest.java
@@ -0,0 +1,105 @@
+package io.quarkus.oidc.db.token.state.manager;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URL;
+import java.util.List;
+import java.util.function.Consumer;
+
+import org.hamcrest.Matchers;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.Test;
+
+import com.gargoylesoftware.htmlunit.SilentCssErrorHandler;
+import com.gargoylesoftware.htmlunit.TextPage;
+import com.gargoylesoftware.htmlunit.WebClient;
+import com.gargoylesoftware.htmlunit.WebRequest;
+import com.gargoylesoftware.htmlunit.WebResponse;
+import com.gargoylesoftware.htmlunit.html.HtmlForm;
+import com.gargoylesoftware.htmlunit.html.HtmlPage;
+
+import io.quarkus.builder.Version;
+import io.quarkus.maven.dependency.Dependency;
+import io.quarkus.test.QuarkusUnitTest;
+import io.quarkus.test.common.http.TestHTTPResource;
+import io.restassured.RestAssured;
+
+public abstract class AbstractDbTokenStateManagerTest {
+
+ protected static QuarkusUnitTest createQuarkusUnitTest(String reactiveSqlClientExtension) {
+ return createQuarkusUnitTest(reactiveSqlClientExtension, null);
+ }
+
+ protected static QuarkusUnitTest createQuarkusUnitTest(String reactiveSqlClientExtension,
+ Consumer customizer) {
+ return new QuarkusUnitTest()
+ .withApplicationRoot((jar) -> {
+ jar
+ .addClasses(ProtectedResource.class, UnprotectedResource.class, PublicResource.class)
+ .addAsResource("application.properties");
+ if (customizer != null) {
+ customizer.accept(jar);
+ }
+ })
+ .setForcedDependencies(
+ List.of(Dependency.of("io.quarkus", reactiveSqlClientExtension, Version.getVersion())));
+ }
+
+ @TestHTTPResource
+ URL url;
+
+ @Test
+ public void testCodeFlow() throws IOException {
+
+ try (final WebClient webClient = createWebClient()) {
+
+ TextPage textPage = webClient.getPage(url.toString() + "unprotected");
+ assertEquals("unprotected", textPage.getContent());
+
+ HtmlPage page;
+ page = webClient.getPage(url.toString() + "protected");
+
+ assertEquals("Sign in to quarkus", page.getTitleText());
+
+ HtmlForm loginForm = page.getForms().get(0);
+
+ loginForm.getInputByName("username").setValueAttribute("alice");
+ loginForm.getInputByName("password").setValueAttribute("alice");
+
+ textPage = loginForm.getInputByName("login").click();
+
+ assertEquals("alice", textPage.getContent());
+
+ assertTokenStateCount(1);
+
+ webClient.getOptions().setRedirectEnabled(false);
+ WebResponse webResponse = webClient
+ .loadWebResponse(new WebRequest(URI.create(url.toString() + "protected/logout").toURL()));
+ assertEquals(302, webResponse.getStatusCode());
+ assertNull(webClient.getCookieManager().getCookie("q_session"));
+
+ webClient.getCookieManager().clearCookies();
+
+ assertTokenStateCount(0);
+ }
+ }
+
+ protected static void assertTokenStateCount(Integer tokenStateCount) {
+ RestAssured
+ .given()
+ .get("public/db-state-manager-table-content")
+ .then()
+ .statusCode(200)
+ .body(Matchers.is(tokenStateCount.toString()));
+ }
+
+ protected static WebClient createWebClient() {
+ WebClient webClient = new WebClient();
+ webClient.setCssErrorHandler(new SilentCssErrorHandler());
+ return webClient;
+ }
+
+}
diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/Db2DbTokenStateManagerTest.java b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/Db2DbTokenStateManagerTest.java
new file mode 100644
index 00000000000000..84e87744cdf6c9
--- /dev/null
+++ b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/Db2DbTokenStateManagerTest.java
@@ -0,0 +1,20 @@
+package io.quarkus.oidc.db.token.state.manager;
+
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.test.QuarkusUnitTest;
+
+// TODO: this test works and we simply need to run it, however in CI it is going to hit
+// hang detection timeout set by 'quarkus.test.hang-detection-timeout=60', we need to discuss and try
+// to find a way to run it (like allow QuarkusUnitTests to override system property etc.)
+// but it will require separate PR and make changes unrelated to DB Token State Manager
+@EnabledIfSystemProperty(named = "run-db2-db-token-state-manager-test", disabledReason = "Db2 is slow to start", matches = "true")
+public class Db2DbTokenStateManagerTest extends AbstractDbTokenStateManagerTest {
+
+ @RegisterExtension
+ static final QuarkusUnitTest test = createQuarkusUnitTest("quarkus-reactive-db2-client",
+ jar -> jar.addAsResource(new StringAsset(System.getProperty("db2.image")), "container-license-acceptance.txt"));
+
+}
diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/GreetingEntity.java b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/GreetingEntity.java
new file mode 100644
index 00000000000000..1ad66681f11acf
--- /dev/null
+++ b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/GreetingEntity.java
@@ -0,0 +1,18 @@
+package io.quarkus.oidc.db.token.state.manager;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+
+@Table(name = "greeting")
+@Entity
+public class GreetingEntity {
+
+ @Id
+ @GeneratedValue
+ Long id;
+
+ String greeting;
+
+}
diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/GreetingResource.java b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/GreetingResource.java
new file mode 100644
index 00000000000000..5d9feec729ac5c
--- /dev/null
+++ b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/GreetingResource.java
@@ -0,0 +1,36 @@
+package io.quarkus.oidc.db.token.state.manager;
+
+import java.util.Objects;
+
+import jakarta.inject.Inject;
+import jakarta.persistence.EntityManager;
+import jakarta.transaction.Transactional;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+
+import io.quarkus.security.Authenticated;
+
+@Path("/greeting")
+@Authenticated
+public class GreetingResource {
+
+ @Inject
+ EntityManager em;
+
+ @Transactional
+ @Path("/new")
+ @GET
+ public void newGreeting() {
+ var entity = new GreetingEntity();
+ entity.greeting = Objects.requireNonNull("Good day");
+ em.persist(entity);
+ }
+
+ @GET
+ public Object getGreetings() {
+ return em
+ .createNativeQuery("SELECT greeting FROM Greeting")
+ .getResultList()
+ .get(0);
+ }
+}
diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/HibernateOrmPgDbTokenStateManagerTest.java b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/HibernateOrmPgDbTokenStateManagerTest.java
new file mode 100644
index 00000000000000..eaa9390090ae4a
--- /dev/null
+++ b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/HibernateOrmPgDbTokenStateManagerTest.java
@@ -0,0 +1,92 @@
+package io.quarkus.oidc.db.token.state.manager;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.net.URI;
+import java.time.Duration;
+import java.util.List;
+
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import com.gargoylesoftware.htmlunit.WebClient;
+import com.gargoylesoftware.htmlunit.WebRequest;
+import com.gargoylesoftware.htmlunit.WebResponse;
+import com.gargoylesoftware.htmlunit.html.HtmlForm;
+import com.gargoylesoftware.htmlunit.html.HtmlPage;
+
+import io.quarkus.builder.Version;
+import io.quarkus.maven.dependency.Dependency;
+import io.quarkus.test.QuarkusUnitTest;
+import io.restassured.RestAssured;
+
+public class HibernateOrmPgDbTokenStateManagerTest extends AbstractDbTokenStateManagerTest {
+
+ @RegisterExtension
+ static final QuarkusUnitTest test = new QuarkusUnitTest()
+ .withApplicationRoot((jar) -> jar
+ .addClasses(ProtectedResource.class, UnprotectedResource.class, PublicResource.class,
+ GreetingResource.class, GreetingEntity.class, OidcDbTokenStateManagerEntity.class,
+ OidcDbTokenStateManagerResource.class)
+ .addAsResource("hibernate-orm-application.properties", "application.properties"))
+ .setForcedDependencies(
+ List.of(Dependency.of("io.quarkus", "quarkus-reactive-pg-client", Version.getVersion()),
+ Dependency.of("io.quarkus", "quarkus-jdbc-postgresql", Version.getVersion())));
+
+ @Test
+ public void testCodeFlowOnTableNotCreatedByExtension() throws IOException {
+ // also tests that this extension works with Hibernate ORM (creates / updates entity)
+ try (final WebClient webClient = createWebClient()) {
+ HtmlPage page;
+ page = webClient.getPage(url.toString() + "greeting/new");
+
+ assertEquals("Sign in to quarkus", page.getTitleText());
+
+ HtmlForm loginForm = page.getForms().get(0);
+
+ loginForm.getInputByName("username").setValueAttribute("alice");
+ loginForm.getInputByName("password").setValueAttribute("alice");
+
+ page = loginForm.getInputByName("login").click();
+ assertEquals(200, page.getWebResponse().getStatusCode());
+
+ WebResponse webResponse = webClient.loadWebResponse(
+ new WebRequest(URI.create(url.toString() + "greeting").toURL()));
+ assertEquals(200, webResponse.getStatusCode());
+ assertTrue(webResponse.getContentAsString().contains("Good day"));
+
+ webClient.getOptions().setRedirectEnabled(false);
+ webResponse = webClient
+ .loadWebResponse(new WebRequest(URI.create(url.toString() + "protected/logout").toURL()));
+ assertEquals(302, webResponse.getStatusCode());
+ assertNull(webClient.getCookieManager().getCookie("q_session"));
+
+ webClient.getCookieManager().clearCookies();
+ }
+ }
+
+ @Test
+ public void testExpiredTokenDeletion() {
+ assertTokenStateCount(0);
+
+ // create 3 tokens
+ RestAssured
+ .given()
+ .body(3)
+ .post("/token-state-manager-generator")
+ .then()
+ .statusCode(204);
+ assertTokenStateCount(3);
+
+ // make sure expired tokens are deleted
+ Awaitility
+ .await()
+ .ignoreExceptions()
+ .atMost(Duration.ofSeconds(10))
+ .untilAsserted(() -> assertTokenStateCount(0));
+ }
+}
diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/MsSqlDbTokenStateManagerTest.java b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/MsSqlDbTokenStateManagerTest.java
new file mode 100644
index 00000000000000..846e020bf35b1e
--- /dev/null
+++ b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/MsSqlDbTokenStateManagerTest.java
@@ -0,0 +1,14 @@
+package io.quarkus.oidc.db.token.state.manager;
+
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.test.QuarkusUnitTest;
+
+public class MsSqlDbTokenStateManagerTest extends AbstractDbTokenStateManagerTest {
+
+ @RegisterExtension
+ static final QuarkusUnitTest test = createQuarkusUnitTest("quarkus-reactive-mssql-client",
+ jar -> jar.addAsResource(new StringAsset(System.getProperty("mssql.image")), "container-license-acceptance.txt"));
+
+}
diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/MySqlDbTokenStateManagerTest.java b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/MySqlDbTokenStateManagerTest.java
new file mode 100644
index 00000000000000..3a729ac393a785
--- /dev/null
+++ b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/MySqlDbTokenStateManagerTest.java
@@ -0,0 +1,12 @@
+package io.quarkus.oidc.db.token.state.manager;
+
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.test.QuarkusUnitTest;
+
+public class MySqlDbTokenStateManagerTest extends AbstractDbTokenStateManagerTest {
+
+ @RegisterExtension
+ static final QuarkusUnitTest test = createQuarkusUnitTest("quarkus-reactive-mysql-client");
+
+}
diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/OidcDbTokenStateManagerEntity.java b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/OidcDbTokenStateManagerEntity.java
new file mode 100644
index 00000000000000..e8e9c8a1bf435e
--- /dev/null
+++ b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/OidcDbTokenStateManagerEntity.java
@@ -0,0 +1,30 @@
+package io.quarkus.oidc.db.token.state.manager;
+
+import static jakarta.persistence.GenerationType.IDENTITY;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+
+@Table(name = "oidc_db_token_state_manager")
+@Entity
+public class OidcDbTokenStateManagerEntity {
+
+ @Id
+ @GeneratedValue(strategy = IDENTITY)
+ Long id;
+
+ @Column(name = "id_token", length = 4000)
+ String idToken;
+
+ @Column(name = "refresh_token", length = 4000)
+ String refreshToken;
+
+ @Column(name = "access_token", length = 4000)
+ String accessToken;
+
+ @Column(name = "expires_in")
+ Long expiresIn;
+}
diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/OidcDbTokenStateManagerResource.java b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/OidcDbTokenStateManagerResource.java
new file mode 100644
index 00000000000000..7393da328bde68
--- /dev/null
+++ b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/OidcDbTokenStateManagerResource.java
@@ -0,0 +1,33 @@
+package io.quarkus.oidc.db.token.state.manager;
+
+import java.time.Instant;
+
+import jakarta.inject.Inject;
+import jakarta.persistence.EntityManager;
+import jakarta.transaction.Transactional;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+
+@Path("/token-state-manager-generator")
+public class OidcDbTokenStateManagerResource {
+
+ private static final long EXPIRED_EXTRA_GRACE = 30;
+
+ @Inject
+ EntityManager em;
+
+ @Transactional
+ @POST
+ public void create(Long numOfTokens) {
+ long expiresIn5Sec = Instant.now().getEpochSecond() + 5 - EXPIRED_EXTRA_GRACE;
+ for (int i = 0; i < numOfTokens; i++) {
+ var token = new OidcDbTokenStateManagerEntity();
+ token.idToken = "ID TOKEN " + i;
+ token.accessToken = "ACCESS TOKEN " + i;
+ token.refreshToken = "REFRESH TOKEN " + i;
+ token.expiresIn = expiresIn5Sec;
+ em.persist(token);
+ }
+ }
+
+}
diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/OracleDbTokenStateManagerTest.java b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/OracleDbTokenStateManagerTest.java
new file mode 100644
index 00000000000000..fa566c7ab3afe1
--- /dev/null
+++ b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/OracleDbTokenStateManagerTest.java
@@ -0,0 +1,18 @@
+package io.quarkus.oidc.db.token.state.manager;
+
+import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.test.QuarkusUnitTest;
+
+// TODO: this test works and we simply need to run it, however in CI it is going to hit
+// hang detection timeout set by 'quarkus.test.hang-detection-timeout=60', we need to discuss and try
+// to find a way to run it (like allow QuarkusUnitTests to override system property etc.)
+// but it will require separate PR and make changes unrelated to DB Token State Manager
+@EnabledIfSystemProperty(named = "run-db2-db-token-state-manager-test", disabledReason = "Oracle is slow to start", matches = "true")
+public class OracleDbTokenStateManagerTest extends AbstractDbTokenStateManagerTest {
+
+ @RegisterExtension
+ static final QuarkusUnitTest test = createQuarkusUnitTest("quarkus-reactive-oracle-client");
+
+}
diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/PostgresDbTokenStateManagerTest.java b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/PostgresDbTokenStateManagerTest.java
new file mode 100644
index 00000000000000..c38ad37596d669
--- /dev/null
+++ b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/PostgresDbTokenStateManagerTest.java
@@ -0,0 +1,12 @@
+package io.quarkus.oidc.db.token.state.manager;
+
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.test.QuarkusUnitTest;
+
+public class PostgresDbTokenStateManagerTest extends AbstractDbTokenStateManagerTest {
+
+ @RegisterExtension
+ static final QuarkusUnitTest test = createQuarkusUnitTest("quarkus-reactive-pg-client");
+
+}
diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/ProtectedResource.java b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/ProtectedResource.java
new file mode 100644
index 00000000000000..c47c7384853cec
--- /dev/null
+++ b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/ProtectedResource.java
@@ -0,0 +1,31 @@
+package io.quarkus.oidc.db.token.state.manager;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+
+import org.eclipse.microprofile.jwt.JsonWebToken;
+
+import io.quarkus.oidc.IdToken;
+import io.quarkus.security.Authenticated;
+
+@Path("/protected")
+@Authenticated
+public class ProtectedResource {
+
+ @Inject
+ @IdToken
+ JsonWebToken idToken;
+
+ @GET
+ public String getName() {
+ return idToken.getName();
+ }
+
+ @GET
+ @Path("logout")
+ public void logout() {
+ throw new RuntimeException("Logout must be handled by CodeAuthenticationMechanism");
+ }
+
+}
diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/PublicResource.java b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/PublicResource.java
new file mode 100644
index 00000000000000..e0d7b1c4633d88
--- /dev/null
+++ b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/PublicResource.java
@@ -0,0 +1,44 @@
+package io.quarkus.oidc.db.token.state.manager;
+
+import java.util.function.Function;
+
+import javax.annotation.security.PermitAll;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+
+import io.smallrye.mutiny.Uni;
+import io.vertx.sqlclient.Pool;
+import io.vertx.sqlclient.Row;
+import io.vertx.sqlclient.RowSet;
+
+@Path("/public")
+@PermitAll
+public class PublicResource {
+
+ @Inject
+ Pool pool;
+
+ @Path("/db-state-manager-table-content")
+ @GET
+ public Uni getDbStateManagerRowsCount() {
+ return Uni.createFrom().completionStage(pool
+ .query("SELECT COUNT(*) FROM oidc_db_token_state_manager")
+ .execute()
+ .map(new Function, Long>() {
+ @Override
+ public Long apply(RowSet rows) {
+ if (rows != null) {
+ var iterator = rows.iterator();
+ if (iterator.hasNext()) {
+ return iterator.next().getLong(0);
+ }
+ }
+ return 0L;
+ }
+ })
+ .toCompletionStage());
+ }
+
+}
diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/UnprotectedResource.java b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/UnprotectedResource.java
new file mode 100644
index 00000000000000..830c5a56892f54
--- /dev/null
+++ b/extensions/oidc-db-token-state-manager/deployment/src/test/java/io/quarkus/oidc/db/token/state/manager/UnprotectedResource.java
@@ -0,0 +1,13 @@
+package io.quarkus.oidc.db.token.state.manager;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+
+@Path("/unprotected")
+public class UnprotectedResource {
+
+ @GET
+ public String getName() {
+ return "unprotected";
+ }
+}
diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/resources/application.properties b/extensions/oidc-db-token-state-manager/deployment/src/test/resources/application.properties
new file mode 100644
index 00000000000000..39d2d3af849201
--- /dev/null
+++ b/extensions/oidc-db-token-state-manager/deployment/src/test/resources/application.properties
@@ -0,0 +1,8 @@
+quarkus.oidc.client-id=quarkus-web-app
+quarkus.oidc.application-type=web-app
+quarkus.oidc.logout.path=/protected/logout
+quarkus.log.category."com.gargoylesoftware.htmlunit.javascript.host.css.CSSStyleSheet".level=FATAL
+quarkus.log.category."com.gargoylesoftware.htmlunit.css".level=FATAL
+quarkus.hibernate-orm.enabled=false
+quarkus.datasource.jdbc=false
+quarkus.oidc-db-token-state-manager.delete-expired=false
\ No newline at end of file
diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/resources/container-license-acceptance.txt b/extensions/oidc-db-token-state-manager/deployment/src/test/resources/container-license-acceptance.txt
new file mode 100644
index 00000000000000..f485095721de00
--- /dev/null
+++ b/extensions/oidc-db-token-state-manager/deployment/src/test/resources/container-license-acceptance.txt
@@ -0,0 +1 @@
+${mssql.image}
\ No newline at end of file
diff --git a/extensions/oidc-db-token-state-manager/deployment/src/test/resources/hibernate-orm-application.properties b/extensions/oidc-db-token-state-manager/deployment/src/test/resources/hibernate-orm-application.properties
new file mode 100644
index 00000000000000..06c67bdda80c2d
--- /dev/null
+++ b/extensions/oidc-db-token-state-manager/deployment/src/test/resources/hibernate-orm-application.properties
@@ -0,0 +1,7 @@
+quarkus.oidc.client-id=quarkus-web-app
+quarkus.oidc.application-type=web-app
+quarkus.oidc.logout.path=/protected/logout
+quarkus.log.category."com.gargoylesoftware.htmlunit.javascript.host.css.CSSStyleSheet".level=FATAL
+quarkus.log.category."com.gargoylesoftware.htmlunit.css".level=FATAL
+quarkus.oidc-db-token-state-manager.delete-expired-delay=3
+quarkus.oidc-db-token-state-manager.create-database-table-if-not-exists=false
\ No newline at end of file
diff --git a/extensions/oidc-db-token-state-manager/pom.xml b/extensions/oidc-db-token-state-manager/pom.xml
new file mode 100644
index 00000000000000..1e7429381d95f3
--- /dev/null
+++ b/extensions/oidc-db-token-state-manager/pom.xml
@@ -0,0 +1,20 @@
+
+
+
+ quarkus-extensions-parent
+ io.quarkus
+ 999-SNAPSHOT
+ ../pom.xml
+
+ 4.0.0
+
+ quarkus-oidc-db-token-state-manager-parent
+ Quarkus - OpenID Connect Database Token State Manager - Parent
+ pom
+
+ deployment
+ runtime
+
+
diff --git a/extensions/oidc-db-token-state-manager/runtime/pom.xml b/extensions/oidc-db-token-state-manager/runtime/pom.xml
new file mode 100644
index 00000000000000..4b42f1dd76bde5
--- /dev/null
+++ b/extensions/oidc-db-token-state-manager/runtime/pom.xml
@@ -0,0 +1,46 @@
+
+
+
+ quarkus-oidc-db-token-state-manager-parent
+ io.quarkus
+ 999-SNAPSHOT
+ ../
+
+ 4.0.0
+
+ quarkus-oidc-db-token-state-manager
+ Quarkus - OpenID Connect Database Token State Manager - Runtime
+ Store an OpenID Connect token state in a database
+
+
+ io.quarkus
+ quarkus-oidc
+
+
+ io.vertx
+ vertx-sql-client
+
+
+
+
+
+ io.quarkus
+ quarkus-extension-maven-plugin
+
+
+ maven-compiler-plugin
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ ${project.version}
+
+
+
+
+
+
+
diff --git a/extensions/oidc-db-token-state-manager/runtime/src/main/java/io/quarkus/oidc/db/token/state/manager/runtime/OidcDbTokenStateManager.java b/extensions/oidc-db-token-state-manager/runtime/src/main/java/io/quarkus/oidc/db/token/state/manager/runtime/OidcDbTokenStateManager.java
new file mode 100644
index 00000000000000..d3f6b3b303bcdf
--- /dev/null
+++ b/extensions/oidc-db-token-state-manager/runtime/src/main/java/io/quarkus/oidc/db/token/state/manager/runtime/OidcDbTokenStateManager.java
@@ -0,0 +1,176 @@
+package io.quarkus.oidc.db.token.state.manager.runtime;
+
+import static io.quarkus.oidc.runtime.CodeAuthenticationMechanism.SESSION_MAX_AGE_PARAM;
+import static io.quarkus.oidc.runtime.OidcUtils.decryptToken;
+import static java.lang.Long.parseLong;
+
+import java.time.Instant;
+import java.util.function.Function;
+
+import org.jboss.logging.Logger;
+
+import io.quarkus.oidc.AuthorizationCodeTokens;
+import io.quarkus.oidc.OidcRequestContext;
+import io.quarkus.oidc.OidcTenantConfig;
+import io.quarkus.oidc.TokenStateManager;
+import io.quarkus.oidc.runtime.OidcUtils;
+import io.quarkus.security.AuthenticationCompletionException;
+import io.smallrye.mutiny.Uni;
+import io.vertx.ext.web.RoutingContext;
+import io.vertx.sqlclient.Pool;
+import io.vertx.sqlclient.Row;
+import io.vertx.sqlclient.RowIterator;
+import io.vertx.sqlclient.RowSet;
+import io.vertx.sqlclient.Tuple;
+
+public class OidcDbTokenStateManager implements TokenStateManager {
+
+ private static final Logger LOG = Logger.getLogger(OidcDbTokenStateManager.class);
+ private static final String TOKEN_STATE_INSERT_FAILED = "Failed to insert token state into database";
+ private static final String FAILED_TO_ACQUIRE_TOKEN = "Failed to acquire authorization code tokens";
+ private final String insertStatement;
+ private final Function, Long> rowsToPrimaryKey;
+ private Pool pool;
+
+ OidcDbTokenStateManager(String insertStatement, Function, Long> rowsToPrimaryKey) {
+ this.insertStatement = insertStatement;
+ this.rowsToPrimaryKey = rowsToPrimaryKey;
+ }
+
+ void setSqlClientPool(Pool pool) {
+ this.pool = pool;
+ }
+
+ @Override
+ public Uni createTokenState(RoutingContext event, OidcTenantConfig oidcConfig,
+ AuthorizationCodeTokens tokens, OidcRequestContext requestContext) {
+ return Uni
+ .createFrom()
+ .completionStage(
+ pool
+ .withTransaction(client -> client
+ .preparedQuery(insertStatement)
+ .execute(
+ Tuple.of(tokens.getIdToken(), tokens.getAccessToken(), tokens.getRefreshToken(),
+ expiresIn(event, oidcConfig))))
+ .toCompletionStage())
+ .onFailure().transform(new Function() {
+ @Override
+ public Throwable apply(Throwable throwable) {
+ return new AuthenticationCompletionException(TOKEN_STATE_INSERT_FAILED, throwable);
+ }
+ })
+ .flatMap(new Function, Uni extends String>>() {
+ @Override
+ public Uni extends String> apply(RowSet rows) {
+ if (rows != null) {
+ return encryptToken(rows, event);
+ }
+ return Uni.createFrom().failure(new AuthenticationCompletionException(TOKEN_STATE_INSERT_FAILED));
+ }
+ })
+ .memoize().indefinitely();
+ }
+
+ @Override
+ public Uni getTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState,
+ OidcRequestContext requestContext) {
+ final long stateId;
+ try {
+ stateId = decryptTokenStateId(tokenState, routingContext);
+ } catch (NumberFormatException ignored) {
+ return Uni.createFrom().failure(
+ new AuthenticationCompletionException("Failed to acquire tokens as the state id is malformed"));
+ }
+ return Uni
+ .createFrom()
+ .completionStage(
+ pool
+ .query(String.format("SELECT id_token, access_token, refresh_token FROM " +
+ "oidc_db_token_state_manager WHERE id = %d AND expires_in >= %d", stateId, now()))
+ .execute()
+ .toCompletionStage())
+ .onFailure().transform(new Function() {
+ @Override
+ public Throwable apply(Throwable throwable) {
+ return new AuthenticationCompletionException(FAILED_TO_ACQUIRE_TOKEN, throwable);
+ }
+ })
+ .flatMap(new Function, Uni extends AuthorizationCodeTokens>>() {
+ @Override
+ public Uni extends AuthorizationCodeTokens> apply(RowSet rows) {
+ if (rows != null) {
+ final RowIterator iterator = rows.iterator();
+ if (iterator.hasNext()) {
+ final Row firstRow = iterator.next();
+ return Uni
+ .createFrom()
+ .item(new AuthorizationCodeTokens(
+ firstRow.getString("id_token"),
+ firstRow.getString("access_token"),
+ firstRow.getString("refresh_token")));
+ }
+ }
+ return Uni.createFrom().failure(new AuthenticationCompletionException(FAILED_TO_ACQUIRE_TOKEN));
+ }
+ })
+ .memoize().indefinitely();
+ }
+
+ @Override
+ public Uni deleteTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState,
+ OidcRequestContext requestContext) {
+ final long stateId;
+ try {
+ stateId = decryptTokenStateId(tokenState, routingContext);
+ } catch (NumberFormatException ignored) {
+ return Uni.createFrom().failure(
+ new AuthenticationCompletionException("Cannot delete token state as the id is malformed"));
+ }
+ return Uni
+ .createFrom()
+ .completionStage(pool
+ .query("DELETE FROM oidc_db_token_state_manager WHERE id = " + stateId)
+ .execute()
+ .toCompletionStage())
+ .replaceWithVoid()
+ .onFailure()
+ .recoverWithItem(new Function() {
+ @Override
+ public Void apply(Throwable throwable) {
+ LOG.debugf("Failed to delete tokens: %s", throwable.getMessage());
+ return null;
+ }
+ });
+ }
+
+ private Uni encryptToken(RowSet rows, RoutingContext context) {
+ Long pk = rowsToPrimaryKey.apply(rows);
+ if (pk == null) {
+ return Uni.createFrom()
+ .failure(new AuthenticationCompletionException("Failed to retrieve token state primary key"));
+ }
+ return Uni.createFrom().item(OidcUtils.encryptToken(Long.toString(pk), context));
+ }
+
+ static long now() {
+ return Instant.now().getEpochSecond();
+ }
+
+ private static long decryptTokenStateId(String tokenStateId, RoutingContext routingContext) {
+ return parseLong(decryptToken(tokenStateId, routingContext));
+ }
+
+ private static long expiresIn(RoutingContext event, OidcTenantConfig oidcConfig) {
+ Long maxAge = event. get(SESSION_MAX_AGE_PARAM);
+ if (maxAge == null) {
+ // illegal state that shouldn't happen
+ throw new AuthenticationCompletionException(SESSION_MAX_AGE_PARAM + " parameter is missing");
+ }
+ maxAge += oidcConfig.token.lifespanGrace.orElse(0);
+ if (oidcConfig.token.refreshExpired) {
+ maxAge += oidcConfig.authentication.sessionAgeExtension.getSeconds();
+ }
+ return now() + maxAge;
+ }
+}
diff --git a/extensions/oidc-db-token-state-manager/runtime/src/main/java/io/quarkus/oidc/db/token/state/manager/runtime/OidcDbTokenStateManagerInitializer.java b/extensions/oidc-db-token-state-manager/runtime/src/main/java/io/quarkus/oidc/db/token/state/manager/runtime/OidcDbTokenStateManagerInitializer.java
new file mode 100644
index 00000000000000..821297405fda67
--- /dev/null
+++ b/extensions/oidc-db-token-state-manager/runtime/src/main/java/io/quarkus/oidc/db/token/state/manager/runtime/OidcDbTokenStateManagerInitializer.java
@@ -0,0 +1,182 @@
+package io.quarkus.oidc.db.token.state.manager.runtime;
+
+import static io.quarkus.oidc.db.token.state.manager.runtime.OidcDbTokenStateManager.now;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import jakarta.enterprise.event.Observes;
+
+import org.jboss.logging.Logger;
+
+import io.quarkus.runtime.StartupEvent;
+import io.smallrye.mutiny.Uni;
+import io.vertx.core.Handler;
+import io.vertx.core.Vertx;
+import io.vertx.sqlclient.Pool;
+import io.vertx.sqlclient.Row;
+import io.vertx.sqlclient.RowSet;
+
+public class OidcDbTokenStateManagerInitializer {
+
+ private static final Logger LOG = Logger.getLogger(OidcDbTokenStateManagerInitializer.class);
+ private static final String FAILED_TO_CREATE_DB_TABLE = "unknown reason, please report the issue and create table manually";
+ /**
+ * Extra 30 seconds before we delete expired tokens.
+ */
+ private static final long EXPIRED_EXTRA_GRACE = 30;
+
+ void initialize(@Observes StartupEvent event, OidcDbTokenStateManagerRunTimeConfig config, Vertx vertx, Pool pool,
+ OidcDbTokenStateManagerInitializerProperties initializerProps) {
+ if (config.createDatabaseTableIfNotExists()) {
+ createDatabaseTable(pool, initializerProps.createTableDdl, initializerProps.supportsIfTableNotExists);
+ }
+ if (initializerProps.isOracleClient) {
+ createOracleInsertFunction(pool);
+ }
+ if (config.deleteExpired()) {
+ periodicallyDeleteExpiredTokens(vertx, pool, config.deleteExpiredDelay().toMillis());
+ }
+ }
+
+ private static void createOracleInsertFunction(Pool pool) {
+ Uni
+ .createFrom()
+ .completionStage(
+ pool
+ .query("CREATE FUNCTION INSERT_TOKENS (id_token_in VARCHAR2, access_token_in " +
+ "VARCHAR2, refresh_token_in " +
+ "VARCHAR2, expires_in_in NUMBER) RETURN NUMBER IS new_id NUMBER; " +
+ "PRAGMA AUTONOMOUS_TRANSACTION;\n" +
+ " BEGIN \n" +
+ " INSERT INTO oidc_db_token_state_manager (id_token, access_token, " +
+ "refresh_token, expires_in) VALUES (id_token_in, access_token_in, " +
+ "refresh_token_in, expires_in_in) RETURNING id INTO new_id;\n" +
+ " COMMIT;\n" +
+ " RETURN(new_id); \n" +
+ " END;")
+ .execute()
+ .toCompletionStage())
+ .onFailure().transform(new Function() {
+ @Override
+ public Throwable apply(Throwable throwable) {
+ return new RuntimeException("Failed to create function for inserting token states into" +
+ " database: " + throwable.getMessage());
+ }
+ })
+ .await()
+ .indefinitely();
+ }
+
+ private static void periodicallyDeleteExpiredTokens(Vertx vertx, Pool pool, long delayBetweenChecks) {
+ vertx
+ .setPeriodic(5000, delayBetweenChecks, new Handler() {
+
+ private final AtomicBoolean deleteInProgress = new AtomicBoolean(false);
+
+ @Override
+ public void handle(Long aLong) {
+ if (deleteInProgress.compareAndSet(false, true)) {
+
+ final long deleteExpiresIn = now() - EXPIRED_EXTRA_GRACE;
+ Uni.createFrom().completionStage(
+ pool
+ .query("DELETE FROM oidc_db_token_state_manager WHERE expires_in < "
+ + deleteExpiresIn)
+ .execute()
+ .toCompletionStage())
+ .subscribe()
+ .with(
+ new Consumer>() {
+ @Override
+ public void accept(RowSet ignored) {
+ // success
+ deleteInProgress.set(false);
+ }
+ },
+ new Consumer() {
+ @Override
+ public void accept(Throwable t) {
+ LOG.errorf("Failed to expired OIDC token states from database: %s",
+ t.getMessage());
+ deleteInProgress.set(false);
+ }
+ });
+ }
+ }
+ });
+ }
+
+ private static void createDatabaseTable(Pool pool, String createTableDdl, boolean supportsIfTableNotExists) {
+ LOG.debugf("Creating database table with query: %s", createTableDdl);
+ String errMsg = Uni
+ .createFrom()
+ .completionStage(
+ pool
+ .query(createTableDdl)
+ .execute()
+ .toCompletionStage())
+ .onItemOrFailure()
+ .transformToUni(new BiFunction, Throwable, Uni extends String>>() {
+ @Override
+ public Uni apply(RowSet rows, Throwable throwable) {
+ if (throwable != null) {
+ if (supportsIfTableNotExists) {
+ return Uni.createFrom().item(throwable.getMessage());
+ } else {
+ // most likely we tried to create table even though it already exists
+ return Uni.createFrom().nullItem();
+ }
+ }
+ // assert table exists
+ return Uni
+ .createFrom()
+ .completionStage(pool
+ // use MAX in order to limit response size
+ // and LIMIT clause is not supported by all the databases
+ .query("SELECT MAX(id) FROM oidc_db_token_state_manager")
+ .execute()
+ .toCompletionStage())
+ .map(new Function, String>() {
+ @Override
+ public String apply(RowSet rows) {
+ if (rows != null && rows.columnsNames().size() == 1) {
+ // table exists
+ return null;
+ }
+ // table does not exist
+ return FAILED_TO_CREATE_DB_TABLE;
+ }
+ })
+ .onFailure().recoverWithItem(new Function() {
+ @Override
+ public String apply(Throwable throwable) {
+ LOG.error("Create database query failed with: ", throwable);
+ return FAILED_TO_CREATE_DB_TABLE;
+ }
+ });
+ }
+ })
+ .await()
+ .indefinitely();
+ if (errMsg != null) {
+ throw new RuntimeException("OIDC Token State Manager failed to create database table: " + errMsg);
+ }
+ }
+
+ public static final class OidcDbTokenStateManagerInitializerProperties {
+
+ private final String createTableDdl;
+ private final boolean supportsIfTableNotExists;
+ private final boolean isOracleClient;
+
+ OidcDbTokenStateManagerInitializerProperties(String createTableDdl, boolean supportsIfTableNotExists,
+ boolean isOracleClient) {
+ this.createTableDdl = createTableDdl;
+ this.supportsIfTableNotExists = supportsIfTableNotExists;
+ this.isOracleClient = isOracleClient;
+ }
+ }
+}
diff --git a/extensions/oidc-db-token-state-manager/runtime/src/main/java/io/quarkus/oidc/db/token/state/manager/runtime/OidcDbTokenStateManagerRecorder.java b/extensions/oidc-db-token-state-manager/runtime/src/main/java/io/quarkus/oidc/db/token/state/manager/runtime/OidcDbTokenStateManagerRecorder.java
new file mode 100644
index 00000000000000..80f67836ba2b32
--- /dev/null
+++ b/extensions/oidc-db-token-state-manager/runtime/src/main/java/io/quarkus/oidc/db/token/state/manager/runtime/OidcDbTokenStateManagerRecorder.java
@@ -0,0 +1,99 @@
+package io.quarkus.oidc.db.token.state.manager.runtime;
+
+import static io.quarkus.oidc.runtime.OidcUtils.DEFAULT_TENANT_ID;
+
+import java.util.Map;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import io.quarkus.arc.runtime.BeanContainer;
+import io.quarkus.oidc.OidcTenantConfig;
+import io.quarkus.oidc.db.token.state.manager.runtime.OidcDbTokenStateManagerInitializer.OidcDbTokenStateManagerInitializerProperties;
+import io.quarkus.oidc.runtime.OidcConfig;
+import io.quarkus.runtime.RuntimeValue;
+import io.quarkus.runtime.annotations.Recorder;
+import io.quarkus.runtime.configuration.ConfigurationException;
+import io.vertx.sqlclient.Pool;
+import io.vertx.sqlclient.PropertyKind;
+import io.vertx.sqlclient.Row;
+import io.vertx.sqlclient.RowSet;
+
+@Recorder
+public class OidcDbTokenStateManagerRecorder {
+
+ private final RuntimeValue oidcConfigRuntimeValue;
+
+ public OidcDbTokenStateManagerRecorder(RuntimeValue oidcConfigRuntimeValue) {
+ this.oidcConfigRuntimeValue = oidcConfigRuntimeValue;
+ }
+
+ /* STATIC INIT */
+ public Supplier createTokenStateManager(String insertStatement, boolean isMySQL) {
+ final Function, Long> rowsToPrimaryKey;
+ if (isMySQL) {
+ rowsToPrimaryKey = new Function, Long>() {
+
+ private final PropertyKind LAST_INSERTED_ID = PropertyKind.create("last-inserted-id", Long.class);
+
+ @Override
+ public Long apply(RowSet rows) {
+ return rows.property(LAST_INSERTED_ID);
+ }
+ };
+ } else {
+ rowsToPrimaryKey = new Function, Long>() {
+ @Override
+ public Long apply(RowSet rows) {
+ var iterator = rows.iterator();
+ if (iterator.hasNext()) {
+ return iterator.next().getLong(0);
+ }
+ throw new IllegalArgumentException(
+ "Token state insertion into database table 'oidc_db_token_state_manager' " +
+ "did not return exactly one result");
+ }
+ };
+ }
+ return new Supplier() {
+ @Override
+ public OidcDbTokenStateManager get() {
+ return new OidcDbTokenStateManager(insertStatement, rowsToPrimaryKey);
+ }
+ };
+ }
+
+ /* RUNTIME INIT */
+ public void validateOidcTenantConfig() {
+ var oidcConfig = oidcConfigRuntimeValue.getValue();
+ for (Map.Entry entry : oidcConfig.namedTenants.entrySet()) {
+ validateEncryptionIsRequired(entry.getValue(), entry.getKey());
+ }
+ validateEncryptionIsRequired(oidcConfig.defaultTenant, DEFAULT_TENANT_ID);
+ }
+
+ /* RUNTIME INIT */
+ public void setSqlClientPool(BeanContainer container) {
+ container.beanInstance(OidcDbTokenStateManager.class).setSqlClientPool(container.beanInstance(Pool.class));
+ }
+
+ /* STATIC INIT */
+ public Supplier createDbTokenStateInitializerProps(String createTableDdl,
+ boolean supportsIfTableNotExists, boolean isOracleClient) {
+ return new Supplier() {
+ @Override
+ public OidcDbTokenStateManagerInitializerProperties get() {
+ return new OidcDbTokenStateManagerInitializerProperties(createTableDdl, supportsIfTableNotExists,
+ isOracleClient);
+ }
+ };
+ }
+
+ private static void validateEncryptionIsRequired(OidcTenantConfig oidcTenantConfig, String tenantName) {
+ if (oidcTenantConfig != null && oidcTenantConfig.tenantEnabled
+ && !oidcTenantConfig.tokenStateManager.encryptionRequired) {
+ throw new ConfigurationException("The OpenID Connect Database Token State Manager requires encrypted "
+ + "token session cookie, however the 'quarkus.oidc.token-state-manager.encryption-required' is "
+ + "disabled for the '" + tenantName + "' tenant.");
+ }
+ }
+}
diff --git a/extensions/oidc-db-token-state-manager/runtime/src/main/java/io/quarkus/oidc/db/token/state/manager/runtime/OidcDbTokenStateManagerRunTimeConfig.java b/extensions/oidc-db-token-state-manager/runtime/src/main/java/io/quarkus/oidc/db/token/state/manager/runtime/OidcDbTokenStateManagerRunTimeConfig.java
new file mode 100644
index 00000000000000..4812ed4b598ea4
--- /dev/null
+++ b/extensions/oidc-db-token-state-manager/runtime/src/main/java/io/quarkus/oidc/db/token/state/manager/runtime/OidcDbTokenStateManagerRunTimeConfig.java
@@ -0,0 +1,33 @@
+package io.quarkus.oidc.db.token.state.manager.runtime;
+
+import static io.quarkus.runtime.annotations.ConfigPhase.RUN_TIME;
+
+import java.time.Duration;
+
+import io.quarkus.runtime.annotations.ConfigRoot;
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithDefault;
+
+@ConfigMapping(prefix = "quarkus.oidc-db-token-state-manager")
+@ConfigRoot(phase = RUN_TIME)
+public interface OidcDbTokenStateManagerRunTimeConfig {
+
+ /**
+ * Whether Quarkus should delete expired token states.
+ * You can disable this feature when you want only one of your service instances to delete expired tokens.
+ */
+ @WithDefault("true")
+ boolean deleteExpired();
+
+ /**
+ * How often should Quarkus check for expired tokens. By default, the Quarkus will check every 20 seconds.
+ */
+ @WithDefault("20")
+ Duration deleteExpiredDelay();
+
+ /**
+ * Whether Quarkus should attempt to create database table where the token state is going to be stored.
+ */
+ @WithDefault("true")
+ boolean createDatabaseTableIfNotExists();
+}
diff --git a/extensions/oidc-db-token-state-manager/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/oidc-db-token-state-manager/runtime/src/main/resources/META-INF/quarkus-extension.yaml
new file mode 100644
index 00000000000000..fb4805694ece72
--- /dev/null
+++ b/extensions/oidc-db-token-state-manager/runtime/src/main/resources/META-INF/quarkus-extension.yaml
@@ -0,0 +1,16 @@
+---
+artifact: ${project.groupId}:${project.artifactId}:${project.version}
+name: "OpenID Connect Database Token State Manager"
+metadata:
+ keywords:
+ - "oauth2"
+ - "openid-connect"
+ - "oidc"
+ - "oidc-token"
+ - "oidc-db-token-state-manager"
+ guide: "https://quarkus.io/guides/security-openid-connect-client"
+ categories:
+ - "security"
+ status: "preview"
+ config:
+ - "quarkus.oidc-db-token-state-manager."
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 be150ea8a97a0c..1dbf394b2da866 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
@@ -60,13 +60,13 @@
public class CodeAuthenticationMechanism extends AbstractOidcAuthenticationMechanism {
+ public static final String SESSION_MAX_AGE_PARAM = "session-max-age";
static final String AMP = "&";
static final String EQ = "=";
static final String COMMA = ",";
static final String UNDERSCORE = "_";
static final String COOKIE_DELIM = "|";
static final Pattern COOKIE_PATTERN = Pattern.compile("\\" + COOKIE_DELIM);
- static final String SESSION_MAX_AGE_PARAM = "session-max-age";
static final String STATE_COOKIE_RESTORE_PATH = "restore-path";
static final Uni VOID_UNI = Uni.createFrom().voidItem();
static final Integer MAX_COOKIE_VALUE_LENGTH = 4096;
diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenStateManager.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenStateManager.java
index fcc50bfe52a5f0..c90d5d07135488 100644
--- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenStateManager.java
+++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenStateManager.java
@@ -7,7 +7,6 @@
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.TokenStateManager;
import io.quarkus.security.AuthenticationCompletionException;
-import io.quarkus.security.AuthenticationFailedException;
import io.smallrye.mutiny.Uni;
import io.vertx.core.http.Cookie;
import io.vertx.core.http.impl.ServerCookie;
@@ -147,24 +146,14 @@ private static String getRefreshTokenCookieName(OidcTenantConfig oidcConfig) {
private String encryptToken(String token, RoutingContext context, OidcTenantConfig oidcConfig) {
if (oidcConfig.tokenStateManager.encryptionRequired) {
- TenantConfigContext configContext = context.get(TenantConfigContext.class.getName());
- try {
- return OidcUtils.encryptString(token, configContext.getTokenEncSecretKey());
- } catch (Exception ex) {
- throw new AuthenticationFailedException(ex);
- }
+ OidcUtils.encryptToken(token, context);
}
return token;
}
private String decryptToken(String token, RoutingContext context, OidcTenantConfig oidcConfig) {
if (oidcConfig.tokenStateManager.encryptionRequired) {
- TenantConfigContext configContext = context.get(TenantConfigContext.class.getName());
- try {
- return OidcUtils.decryptString(token, configContext.getTokenEncSecretKey());
- } catch (Exception ex) {
- throw new AuthenticationFailedException(ex);
- }
+ return OidcUtils.decryptToken(token, context);
}
return 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 fa5164a21cdc5b..8a3ead3d1ac735 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
@@ -532,6 +532,24 @@ public static String decryptString(String jweString, Key key) throws Exception {
return decryptString(jweString, key, KeyEncryptionAlgorithm.A256GCMKW);
}
+ public static String decryptToken(String token, RoutingContext context) {
+ TenantConfigContext configContext = context.get(TenantConfigContext.class.getName());
+ try {
+ return OidcUtils.decryptString(token, configContext.getTokenEncSecretKey());
+ } catch (Exception ex) {
+ throw new AuthenticationFailedException(ex);
+ }
+ }
+
+ public static String encryptToken(String token, RoutingContext context) {
+ TenantConfigContext configContext = context.get(TenantConfigContext.class.getName());
+ try {
+ return OidcUtils.encryptString(token, configContext.getTokenEncSecretKey());
+ } catch (Exception ex) {
+ throw new AuthenticationFailedException(ex);
+ }
+ }
+
public static String decryptString(String jweString, Key key, KeyEncryptionAlgorithm algorithm) throws JoseException {
JsonWebEncryption jwe = new JsonWebEncryption();
jwe.setAlgorithmConstraints(new AlgorithmConstraints(AlgorithmConstraints.ConstraintType.PERMIT,
diff --git a/extensions/pom.xml b/extensions/pom.xml
index 81f2fbfa8d0848..4ebe3559695e1f 100644
--- a/extensions/pom.xml
+++ b/extensions/pom.xml
@@ -141,6 +141,7 @@
oidc-client-reactive-filter
oidc-token-propagation
oidc-token-propagation-reactive
+ oidc-db-token-state-manager
keycloak-authorization
keycloak-admin-client-common
keycloak-admin-client
diff --git a/extensions/reactive-db2-client/runtime/pom.xml b/extensions/reactive-db2-client/runtime/pom.xml
index 8ea8d89f8db39c..254cda26cb106e 100644
--- a/extensions/reactive-db2-client/runtime/pom.xml
+++ b/extensions/reactive-db2-client/runtime/pom.xml
@@ -49,6 +49,11 @@
io.quarkus
quarkus-extension-maven-plugin
+
+
+ io.quarkus.reactive-db2-client
+
+
maven-compiler-plugin
diff --git a/extensions/reactive-mssql-client/runtime/pom.xml b/extensions/reactive-mssql-client/runtime/pom.xml
index 8fa6801bdb7fa0..aa9ba57b9f1449 100644
--- a/extensions/reactive-mssql-client/runtime/pom.xml
+++ b/extensions/reactive-mssql-client/runtime/pom.xml
@@ -53,6 +53,11 @@
io.quarkus
quarkus-extension-maven-plugin
+
+
+ io.quarkus.reactive-mssql-client
+
+
maven-compiler-plugin
diff --git a/extensions/reactive-mysql-client/runtime/pom.xml b/extensions/reactive-mysql-client/runtime/pom.xml
index f8229fb01f6491..f98ea175f5550a 100644
--- a/extensions/reactive-mysql-client/runtime/pom.xml
+++ b/extensions/reactive-mysql-client/runtime/pom.xml
@@ -69,6 +69,11 @@
io.quarkus
quarkus-extension-maven-plugin
+
+
+ io.quarkus.reactive-mysql-client
+
+
maven-compiler-plugin
diff --git a/extensions/reactive-oracle-client/runtime/pom.xml b/extensions/reactive-oracle-client/runtime/pom.xml
index 2a07cfd2169f3d..842a846f3c9b11 100644
--- a/extensions/reactive-oracle-client/runtime/pom.xml
+++ b/extensions/reactive-oracle-client/runtime/pom.xml
@@ -57,6 +57,11 @@
io.quarkus
quarkus-extension-maven-plugin
+
+
+ io.quarkus.reactive-oracle-client
+
+
maven-compiler-plugin
diff --git a/extensions/reactive-pg-client/runtime/pom.xml b/extensions/reactive-pg-client/runtime/pom.xml
index 72d18586745532..21ea768d8973b8 100644
--- a/extensions/reactive-pg-client/runtime/pom.xml
+++ b/extensions/reactive-pg-client/runtime/pom.xml
@@ -57,6 +57,11 @@
io.quarkus
quarkus-extension-maven-plugin
+
+
+ io.quarkus.reactive-pg-client
+
+
maven-compiler-plugin