diff --git a/commafeed-server/pom.xml b/commafeed-server/pom.xml
index ad5b67ff..06424992 100644
--- a/commafeed-server/pom.xml
+++ b/commafeed-server/pom.xml
@@ -248,10 +248,6 @@
io.quarkus
quarkus-arc
-
- io.quarkus
- quarkus-reactive-routes
-
io.quarkus
quarkus-security
diff --git a/commafeed-server/src/main/java/com/commafeed/security/FormAuthenticationCookieInterceptor.java b/commafeed-server/src/main/java/com/commafeed/security/FormAuthenticationCookieInterceptor.java
deleted file mode 100644
index 3692ed2d..00000000
--- a/commafeed-server/src/main/java/com/commafeed/security/FormAuthenticationCookieInterceptor.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package com.commafeed.security;
-
-import io.quarkus.vertx.http.runtime.HttpConfiguration;
-import io.quarkus.vertx.http.runtime.security.FormAuthenticationMechanism;
-import io.quarkus.vertx.web.RouteFilter;
-import io.vertx.core.http.Cookie;
-import io.vertx.core.http.impl.ServerCookie;
-import io.vertx.ext.web.RoutingContext;
-import jakarta.inject.Singleton;
-import lombok.RequiredArgsConstructor;
-
-/**
- * Intercepts responses and sets a Max-Age on the cookie created by {@link FormAuthenticationMechanism} because it has no value by default.
- *
- * This is a workaround for https://github.com/quarkusio/quarkus/issues/42463
- */
-@RequiredArgsConstructor
-@Singleton
-public class FormAuthenticationCookieInterceptor {
-
- private final HttpConfiguration config;
-
- @RouteFilter(Integer.MAX_VALUE)
- public void cookieInterceptor(RoutingContext context) {
- context.addHeadersEndHandler(v -> {
- Cookie cookie = context.request().getCookie(config.auth.form.cookieName);
- if (cookie instanceof ServerCookie sc && sc.isChanged()) {
- cookie.setMaxAge(config.auth.form.timeout.toSeconds());
- }
- });
-
- context.next();
- }
-}
diff --git a/commafeed-server/src/main/java/com/commafeed/security/mechanism/CookieMaxAgeFormAuthenticationMechanism.java b/commafeed-server/src/main/java/com/commafeed/security/mechanism/CookieMaxAgeFormAuthenticationMechanism.java
new file mode 100644
index 00000000..c5ffa581
--- /dev/null
+++ b/commafeed-server/src/main/java/com/commafeed/security/mechanism/CookieMaxAgeFormAuthenticationMechanism.java
@@ -0,0 +1,90 @@
+package com.commafeed.security.mechanism;
+
+import java.security.SecureRandom;
+import java.util.Base64;
+
+import io.quarkus.vertx.http.runtime.FormAuthConfig;
+import io.quarkus.vertx.http.runtime.FormAuthRuntimeConfig;
+import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig;
+import io.quarkus.vertx.http.runtime.HttpConfiguration;
+import io.quarkus.vertx.http.runtime.security.FormAuthenticationMechanism;
+import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism;
+import io.quarkus.vertx.http.runtime.security.PersistentLoginManager;
+import io.vertx.core.http.Cookie;
+import io.vertx.core.http.impl.ServerCookie;
+import io.vertx.ext.web.RoutingContext;
+import jakarta.annotation.Priority;
+import jakarta.inject.Singleton;
+import lombok.experimental.Delegate;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * HttpAuthenticationMechanism that wraps FormAuthenticationMechanism and sets a Max-Age on the cookie because it has no value by default.
+ *
+ * This is a workaround for https://github.com/quarkusio/quarkus/issues/42463
+ */
+@Priority(1)
+@Singleton
+@Slf4j
+public class CookieMaxAgeFormAuthenticationMechanism implements HttpAuthenticationMechanism {
+
+ // the temp encryption key, persistent across dev mode restarts
+ static volatile String encryptionKey;
+
+ @Delegate
+ private final FormAuthenticationMechanism delegate;
+
+ public CookieMaxAgeFormAuthenticationMechanism(HttpConfiguration httpConfiguration, HttpBuildTimeConfig buildTimeConfig) {
+ String key;
+ if (httpConfiguration.encryptionKey.isEmpty()) {
+ if (encryptionKey != null) {
+ // persist across dev mode restarts
+ key = encryptionKey;
+ } else {
+ byte[] data = new byte[32];
+ new SecureRandom().nextBytes(data);
+ key = encryptionKey = Base64.getEncoder().encodeToString(data);
+ log.warn("Encryption key was not specified for persistent FORM auth, using temporary key {}", key);
+ }
+ } else {
+ key = httpConfiguration.encryptionKey.get();
+ }
+
+ FormAuthConfig form = buildTimeConfig.auth.form;
+ FormAuthRuntimeConfig runtimeForm = httpConfiguration.auth.form;
+ String loginPage = startWithSlash(runtimeForm.loginPage.orElse(null));
+ String errorPage = startWithSlash(runtimeForm.errorPage.orElse(null));
+ String landingPage = startWithSlash(runtimeForm.landingPage.orElse(null));
+ String postLocation = startWithSlash(form.postLocation);
+ String usernameParameter = runtimeForm.usernameParameter;
+ String passwordParameter = runtimeForm.passwordParameter;
+ String locationCookie = runtimeForm.locationCookie;
+ String cookiePath = runtimeForm.cookiePath.orElse(null);
+ boolean redirectAfterLogin = landingPage != null;
+ String cookieSameSite = runtimeForm.cookieSameSite.name();
+
+ PersistentLoginManager loginManager = new PersistentLoginManager(key, runtimeForm.cookieName, runtimeForm.timeout.toMillis(),
+ runtimeForm.newCookieInterval.toMillis(), runtimeForm.httpOnlyCookie, cookieSameSite, cookiePath) {
+ @Override
+ public void save(String value, RoutingContext context, String cookieName, RestoreResult restoreResult, boolean secureCookie) {
+ super.save(value, context, cookieName, restoreResult, secureCookie);
+
+ // add max age to the cookie
+ Cookie cookie = context.request().getCookie(cookieName);
+ if (cookie instanceof ServerCookie sc && sc.isChanged()) {
+ cookie.setMaxAge(runtimeForm.timeout.toSeconds());
+ }
+ }
+ };
+
+ this.delegate = new FormAuthenticationMechanism(loginPage, postLocation, usernameParameter, passwordParameter, errorPage,
+ landingPage, redirectAfterLogin, locationCookie, cookieSameSite, cookiePath, loginManager);
+ }
+
+ private static String startWithSlash(String page) {
+ if (page == null) {
+ return null;
+ }
+ return page.startsWith("/") ? page : "/" + page;
+ }
+}