From 5d4302b1c5e00c091e272833d362629a74c4e742 Mon Sep 17 00:00:00 2001 From: Marco Collovati Date: Sat, 18 Jan 2025 21:20:18 +0100 Subject: [PATCH] feat: allow configuring logout path and redirect This change allows to configure logout process for Hilla security policy. by setting the logout path, an optional redirect URI after logout and a flag to invalidate the HTTP session. --- .../TypescriptClientCodeGenProvider.java | 4 ++ .../HillaFormAuthenticationMechanism.java | 32 ++++++---- .../hilla/security/HillaSecurityRecorder.java | 17 ++++-- .../hilla/security/VaadinSecurityConfig.java | 58 +++++++++++++++++++ 4 files changed, 94 insertions(+), 17 deletions(-) create mode 100644 commons/runtime/src/main/java/com/github/mcollovati/quarkus/hilla/security/VaadinSecurityConfig.java diff --git a/commons/deployment/src/main/java/com/github/mcollovati/quarkus/hilla/deployment/TypescriptClientCodeGenProvider.java b/commons/deployment/src/main/java/com/github/mcollovati/quarkus/hilla/deployment/TypescriptClientCodeGenProvider.java index 17bfa78c..eb7c0e63 100644 --- a/commons/deployment/src/main/java/com/github/mcollovati/quarkus/hilla/deployment/TypescriptClientCodeGenProvider.java +++ b/commons/deployment/src/main/java/com/github/mcollovati/quarkus/hilla/deployment/TypescriptClientCodeGenProvider.java @@ -117,6 +117,10 @@ public boolean shouldRun(Path sourceDir, Config config) { @Override public boolean trigger(CodeGenContext context) { + if (context.test()) { + // Generation not required for test code + return false; + } String prefix = computeConnectClientPrefix(context.config()); boolean defaultPrefix = "connect".equals(prefix); Path customClient = context.inputDir().resolve("connect-client.ts"); diff --git a/commons/runtime/src/main/java/com/github/mcollovati/quarkus/hilla/security/HillaFormAuthenticationMechanism.java b/commons/runtime/src/main/java/com/github/mcollovati/quarkus/hilla/security/HillaFormAuthenticationMechanism.java index 9d1b89be..7db2d6a3 100644 --- a/commons/runtime/src/main/java/com/github/mcollovati/quarkus/hilla/security/HillaFormAuthenticationMechanism.java +++ b/commons/runtime/src/main/java/com/github/mcollovati/quarkus/hilla/security/HillaFormAuthenticationMechanism.java @@ -31,23 +31,18 @@ import io.vertx.ext.web.RoutingContext; public class HillaFormAuthenticationMechanism implements HttpAuthenticationMechanism { - private final String landingPage; - private final String logoutPath; - private final String cookieName; + private final Config config; FormAuthenticationMechanism delegate; - public HillaFormAuthenticationMechanism( - FormAuthenticationMechanism delegate, String cookieName, String landingPage, String logoutPath) { + public HillaFormAuthenticationMechanism(FormAuthenticationMechanism delegate, Config config) { this.delegate = delegate; - this.landingPage = landingPage; - this.logoutPath = logoutPath; - this.cookieName = cookieName; + this.config = config; } @Override public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { - if (context.normalizedPath().equals(logoutPath)) { + if (context.normalizedPath().equals(config.logoutPath())) { logout(context); return Uni.createFrom().optional(Optional.empty()); } @@ -63,7 +58,7 @@ public Uni authenticate(RoutingContext context, IdentityProvid if (loginFailure) { response.setStatusCode(401); } else { - response.putHeader("Default-url", landingPage); + response.putHeader("Default-url", config.landingPage()); if (redirectUrl != null) { response.putHeader("Saved-url", redirectUrl); } @@ -93,10 +88,23 @@ private void logout(RoutingContext ctx) { // Vert.x sends back a set-cookie with max-age and expiry but no path, // so we have to set it first, // otherwise web clients don't clear it - Cookie cookie = ctx.request().getCookie(cookieName); + Cookie cookie = ctx.request().getCookie(config.cookieName()); if (cookie != null) { cookie.setPath("/"); } - ctx.response().removeCookie(cookieName); + ctx.response().removeCookie(config.cookieName()); + if (config.invalidateSessions() && ctx.session() != null) { + ctx.session().destroy(); + } + if (config.postLogoutRedirect() != null) { + ctx.redirect(config.postLogoutRedirect()); + } } + + public record Config( + String cookieName, + String landingPage, + String logoutPath, + String postLogoutRedirect, + boolean invalidateSessions) {} } diff --git a/commons/runtime/src/main/java/com/github/mcollovati/quarkus/hilla/security/HillaSecurityRecorder.java b/commons/runtime/src/main/java/com/github/mcollovati/quarkus/hilla/security/HillaSecurityRecorder.java index 1a6b3dc5..6d5a9610 100644 --- a/commons/runtime/src/main/java/com/github/mcollovati/quarkus/hilla/security/HillaSecurityRecorder.java +++ b/commons/runtime/src/main/java/com/github/mcollovati/quarkus/hilla/security/HillaSecurityRecorder.java @@ -22,6 +22,7 @@ import io.quarkus.arc.runtime.BeanContainer; import io.quarkus.runtime.annotations.Recorder; import io.quarkus.vertx.http.runtime.security.FormAuthenticationMechanism; +import io.smallrye.config.SmallRyeConfig; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; @@ -31,14 +32,20 @@ public class HillaSecurityRecorder { public Supplier setupFormAuthenticationMechanism() { - String cookieName = ConfigProvider.getConfig().getValue("quarkus.http.auth.form.cookie-name", String.class); - String landingPage = ConfigProvider.getConfig() - .getOptionalValue("quarkus.http.auth.form.landing-page", String.class) - .orElse("/"); + Config config = ConfigProvider.getConfig(); + VaadinSecurityConfig securityConfig = + config.unwrap(SmallRyeConfig.class).getConfigMapping(VaadinSecurityConfig.class); + var authConfig = new HillaFormAuthenticationMechanism.Config( + config.getValue("quarkus.http.auth.form.cookie-name", String.class), + config.getOptionalValue("quarkus.http.auth.form.landing-page", String.class) + .orElse("/"), + securityConfig.logoutPath(), + securityConfig.postLogoutRedirectUri().orElse(null), + securityConfig.logoutInvalidateSession()); return () -> { FormAuthenticationMechanism delegate = Arc.container().instance(FormAuthenticationMechanism.class).get(); - return new HillaFormAuthenticationMechanism(delegate, cookieName, landingPage, "/logout"); + return new HillaFormAuthenticationMechanism(delegate, authConfig); }; } diff --git a/commons/runtime/src/main/java/com/github/mcollovati/quarkus/hilla/security/VaadinSecurityConfig.java b/commons/runtime/src/main/java/com/github/mcollovati/quarkus/hilla/security/VaadinSecurityConfig.java new file mode 100644 index 00000000..888c3ced --- /dev/null +++ b/commons/runtime/src/main/java/com/github/mcollovati/quarkus/hilla/security/VaadinSecurityConfig.java @@ -0,0 +1,58 @@ +/* + * Copyright 2025 Marco Collovati, Dario Götze + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.mcollovati.quarkus.hilla.security; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; + +/** + * Configuration properties for Vaadin security. + */ +@ConfigMapping(prefix = "vaadin.security") +@ConfigRoot(phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED) +interface VaadinSecurityConfig { + + /** + * The path of the logout HTTP POST endpoint handling logout requests. + *

+ * Defaults to {@literal /logout}. + * + * @return the path of the logout endpoint. + */ + @WithDefault("/logout") + String logoutPath(); + + /** + * The post logout redirect uri. + *

+ * @return post logout redirect uri. + */ + Optional postLogoutRedirectUri(); + + /** + * Whether HTTP session should be invalidated on logout. + *

+ * Defaults to {@literal true}. + * + * @return whether HTTP session should be invalidated on logout. + */ + @WithDefault("true") + boolean logoutInvalidateSession(); +}