diff --git a/extensions/security-webauthn/runtime/src/main/java/io/quarkus/security/webauthn/WebAuthnRecorder.java b/extensions/security-webauthn/runtime/src/main/java/io/quarkus/security/webauthn/WebAuthnRecorder.java index c1bc8c8e96053..ed6a7bef94a69 100644 --- a/extensions/security-webauthn/runtime/src/main/java/io/quarkus/security/webauthn/WebAuthnRecorder.java +++ b/extensions/security-webauthn/runtime/src/main/java/io/quarkus/security/webauthn/WebAuthnRecorder.java @@ -69,7 +69,8 @@ public WebAuthnAuthenticationMechanism get() { WebAuthnRunTimeConfig config = WebAuthnRecorder.this.config.getValue(); PersistentLoginManager loginManager = new PersistentLoginManager(key, config.cookieName, config.sessionTimeout.toMillis(), - config.newCookieInterval.toMillis(), false, config.cookieSameSite.name()); + config.newCookieInterval.toMillis(), false, config.cookieSameSite.name(), + config.cookiePath.orElse(null)); String loginPage = config.loginPage.startsWith("/") ? config.loginPage : "/" + config.loginPage; return new WebAuthnAuthenticationMechanism(loginManager, loginPage); } diff --git a/extensions/security-webauthn/runtime/src/main/java/io/quarkus/security/webauthn/WebAuthnRunTimeConfig.java b/extensions/security-webauthn/runtime/src/main/java/io/quarkus/security/webauthn/WebAuthnRunTimeConfig.java index 23a92faae213b..6433f25742006 100644 --- a/extensions/security-webauthn/runtime/src/main/java/io/quarkus/security/webauthn/WebAuthnRunTimeConfig.java +++ b/extensions/security-webauthn/runtime/src/main/java/io/quarkus/security/webauthn/WebAuthnRunTimeConfig.java @@ -9,7 +9,6 @@ import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; -import io.quarkus.vertx.http.runtime.FormAuthConfig.CookieSameSite; import io.vertx.ext.auth.webauthn.Attestation; import io.vertx.ext.auth.webauthn.AuthenticatorAttachment; import io.vertx.ext.auth.webauthn.AuthenticatorTransport; @@ -238,4 +237,10 @@ public static class RelyingPartyConfig { */ @ConfigItem(defaultValue = "strict") public CookieSameSite cookieSameSite = CookieSameSite.STRICT; + + /** + * The cookie path for the session cookies. + */ + @ConfigItem(defaultValue = "/") + public Optional cookiePath = Optional.of("/"); } diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CombinedFormBasicAuthTestCase.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CombinedFormBasicAuthTestCase.java index 6f43065b7b9f2..f7fb2f92d9175 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CombinedFormBasicAuthTestCase.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CombinedFormBasicAuthTestCase.java @@ -63,7 +63,8 @@ public void testFormBasedAuthSuccess() { .assertThat() .statusCode(302) .header("location", containsString("/login")) - .cookie("quarkus-redirect-location", detailedCookie().sameSite("Strict").value(containsString("/admin"))); + .cookie("quarkus-redirect-location", + detailedCookie().sameSite("Strict").path(equalTo("/")).value(containsString("/admin"))); RestAssured .given() @@ -77,7 +78,7 @@ public void testFormBasedAuthSuccess() { .assertThat() .statusCode(302) .header("location", containsString("/admin")) - .cookie("quarkus-credential", detailedCookie().sameSite("Strict")); + .cookie("quarkus-credential", detailedCookie().sameSite("Strict").path(equalTo("/"))); RestAssured .given() diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/FormBasicAuthHttpRootNoCookiePathTestCase.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/FormBasicAuthHttpRootNoCookiePathTestCase.java new file mode 100644 index 0000000000000..9d060952d5572 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/FormBasicAuthHttpRootNoCookiePathTestCase.java @@ -0,0 +1,97 @@ +package io.quarkus.vertx.http.security; + +import static io.restassured.matcher.RestAssuredMatchers.detailedCookie; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +import java.util.function.Supplier; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.restassured.filter.cookie.CookieFilter; + +public class FormBasicAuthHttpRootNoCookiePathTestCase { + + private static final String APP_PROPS = "" + + "quarkus.http.root-path=/root\n" + + "quarkus.http.auth.form.enabled=true\n" + + "quarkus.http.auth.form.login-page=login\n" + + "quarkus.http.auth.form.cookie-path=\n" + + "quarkus.http.auth.form.error-page=error\n" + + "quarkus.http.auth.form.landing-page=landing\n" + + "quarkus.http.auth.policy.r1.roles-allowed=admin\n" + + "quarkus.http.auth.permission.roles1.paths=/root/admin\n" + + "quarkus.http.auth.permission.roles1.policy=r1\n"; + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(TestIdentityProvider.class, TestTrustedIdentityProvider.class, TestIdentityController.class, + PathHandler.class) + .addAsResource(new StringAsset(APP_PROPS), "application.properties"); + } + }); + + @BeforeAll + public static void setup() { + TestIdentityController.resetRoles() + .add("admin", "admin", "admin"); + } + + @Test + public void testFormBasedAuthSuccess() { + CookieFilter cookies = new CookieFilter(); + RestAssured + .given() + .filter(cookies) + .redirects().follow(false) + .when() + .get("/admin") + .then() + .assertThat() + .statusCode(302) + .header("location", containsString("/login")) + .cookie("quarkus-redirect-location", + detailedCookie().value(containsString("/root/admin")).path(nullValue())); + + RestAssured + .given() + .filter(cookies) + .redirects().follow(false) + .when() + .formParam("j_username", "admin") + .formParam("j_password", "admin") + .post("/j_security_check") + .then() + .assertThat() + .statusCode(302) + .header("location", containsString("/root/admin")) + .cookie("quarkus-credential", + detailedCookie().value(notNullValue()).path(nullValue())); + + RestAssured + .given() + .filter(cookies) + .redirects().follow(false) + .when() + .get("/admin") + .then() + .assertThat() + .statusCode(200) + .body(equalTo("admin:/root/admin")); + + } +} diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/FormBasicAuthHttpRootTestCase.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/FormBasicAuthHttpRootTestCase.java new file mode 100644 index 0000000000000..6ba6b6fa89251 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/FormBasicAuthHttpRootTestCase.java @@ -0,0 +1,96 @@ +package io.quarkus.vertx.http.security; + +import static io.restassured.matcher.RestAssuredMatchers.detailedCookie; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; + +import java.util.function.Supplier; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.restassured.filter.cookie.CookieFilter; + +public class FormBasicAuthHttpRootTestCase { + + private static final String APP_PROPS = "" + + "quarkus.http.root-path=/root\n" + + "quarkus.http.auth.form.enabled=true\n" + + "quarkus.http.auth.form.login-page=login\n" + + "quarkus.http.auth.form.cookie-path=/root\n" + + "quarkus.http.auth.form.error-page=error\n" + + "quarkus.http.auth.form.landing-page=landing\n" + + "quarkus.http.auth.policy.r1.roles-allowed=admin\n" + + "quarkus.http.auth.permission.roles1.paths=/root/admin\n" + + "quarkus.http.auth.permission.roles1.policy=r1\n"; + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(TestIdentityProvider.class, TestTrustedIdentityProvider.class, TestIdentityController.class, + PathHandler.class) + .addAsResource(new StringAsset(APP_PROPS), "application.properties"); + } + }); + + @BeforeAll + public static void setup() { + TestIdentityController.resetRoles() + .add("admin", "admin", "admin"); + } + + @Test + public void testFormBasedAuthSuccess() { + CookieFilter cookies = new CookieFilter(); + RestAssured + .given() + .filter(cookies) + .redirects().follow(false) + .when() + .get("/admin") + .then() + .assertThat() + .statusCode(302) + .header("location", containsString("/login")) + .cookie("quarkus-redirect-location", + detailedCookie().value(containsString("/root/admin")).path(equalTo("/root"))); + + RestAssured + .given() + .filter(cookies) + .redirects().follow(false) + .when() + .formParam("j_username", "admin") + .formParam("j_password", "admin") + .post("/j_security_check") + .then() + .assertThat() + .statusCode(302) + .header("location", containsString("/root/admin")) + .cookie("quarkus-credential", + detailedCookie().value(notNullValue()).path(equalTo("/root"))); + + RestAssured + .given() + .filter(cookies) + .redirects().follow(false) + .when() + .get("/admin") + .then() + .assertThat() + .statusCode(200) + .body(equalTo("admin:/root/admin")); + + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FormAuthConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FormAuthConfig.java index e068733f26082..5348c0533c024 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FormAuthConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FormAuthConfig.java @@ -1,6 +1,7 @@ package io.quarkus.vertx.http.runtime; import java.time.Duration; +import java.util.Optional; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -107,6 +108,12 @@ public enum CookieSameSite { @ConfigItem(defaultValue = "quarkus-credential") public String cookieName; + /** + * The cookie path for the session and location cookies. + */ + @ConfigItem(defaultValue = "/") + public Optional cookiePath = Optional.of("/"); + /** * Set the HttpOnly attribute to prevent access to the cookie via JavaScript. */ diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java index a52967b6a4863..024efe8e5f4e2 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java @@ -40,11 +40,14 @@ public class FormAuthenticationMechanism implements HttpAuthenticationMechanism private final String landingPage; private final boolean redirectAfterLogin; private final CookieSameSite cookieSameSite; + private final String cookiePath; + private final PersistentLoginManager loginManager; public FormAuthenticationMechanism(String loginPage, String postLocation, String usernameParameter, String passwordParameter, String errorPage, String landingPage, - boolean redirectAfterLogin, String locationCookie, String cookieSameSite, PersistentLoginManager loginManager) { + boolean redirectAfterLogin, String locationCookie, String cookieSameSite, String cookiePath, + PersistentLoginManager loginManager) { this.loginPage = loginPage; this.postLocation = postLocation; this.usernameParameter = usernameParameter; @@ -54,6 +57,7 @@ public FormAuthenticationMechanism(String loginPage, String postLocation, this.landingPage = landingPage; this.redirectAfterLogin = redirectAfterLogin; this.cookieSameSite = CookieSameSite.valueOf(cookieSameSite); + this.cookiePath = cookiePath; this.loginManager = loginManager; } @@ -149,7 +153,7 @@ protected void verifyRedirectBackLocation(String requestURIString, String redire protected void storeInitialLocation(final RoutingContext exchange) { exchange.response().addCookie(Cookie.cookie(locationCookie, exchange.request().absoluteURI()) - .setPath("/").setSameSite(cookieSameSite).setSecure(exchange.request().isSSL())); + .setPath(cookiePath).setSameSite(cookieSameSite).setSecure(exchange.request().isSSL())); } protected void servePage(final RoutingContext exchange, final String location) { diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java index d56e829a846b5..07415fb062fe8 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java @@ -266,7 +266,8 @@ public FormAuthenticationMechanism get() { } FormAuthConfig form = buildTimeConfig.auth.form; PersistentLoginManager loginManager = new PersistentLoginManager(key, form.cookieName, form.timeout.toMillis(), - form.newCookieInterval.toMillis(), form.httpOnlyCookie, form.cookieSameSite.name()); + form.newCookieInterval.toMillis(), form.httpOnlyCookie, form.cookieSameSite.name(), + form.cookiePath.orElse(null)); String loginPage = form.loginPage.startsWith("/") ? form.loginPage : "/" + form.loginPage; String errorPage = form.errorPage.startsWith("/") ? form.errorPage : "/" + form.errorPage; String landingPage = form.landingPage.startsWith("/") ? form.landingPage : "/" + form.landingPage; @@ -274,9 +275,11 @@ public FormAuthenticationMechanism get() { String usernameParameter = form.usernameParameter; String passwordParameter = form.passwordParameter; String locationCookie = form.locationCookie; + String cookiePath = form.cookiePath.orElse(null); boolean redirectAfterLogin = form.redirectAfterLogin; return new FormAuthenticationMechanism(loginPage, postLocation, usernameParameter, passwordParameter, - errorPage, landingPage, redirectAfterLogin, locationCookie, form.cookieSameSite.name(), loginManager); + errorPage, landingPage, redirectAfterLogin, locationCookie, form.cookieSameSite.name(), cookiePath, + loginManager); } }; } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PersistentLoginManager.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PersistentLoginManager.java index 48b52baafb3f8..279e9c3970f00 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PersistentLoginManager.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PersistentLoginManager.java @@ -38,14 +38,16 @@ public class PersistentLoginManager { private final long newCookieIntervalMillis; private final boolean httpOnlyCookie; private final CookieSameSite cookieSameSite; + private final String cookiePath; public PersistentLoginManager(String encryptionKey, String cookieName, long timeoutMillis, long newCookieIntervalMillis, - boolean httpOnlyCookie, String cookieSameSite) { + boolean httpOnlyCookie, String cookieSameSite, String cookiePath) { this.cookieName = cookieName; this.newCookieIntervalMillis = newCookieIntervalMillis; this.timeoutMillis = timeoutMillis; this.httpOnlyCookie = httpOnlyCookie; this.cookieSameSite = CookieSameSite.valueOf(cookieSameSite); + this.cookiePath = cookiePath; try { if (encryptionKey == null) { this.secretKey = KeyGenerator.getInstance("AES").generateKey(); @@ -138,7 +140,8 @@ public void save(String value, RoutingContext context, String cookieName, Restor message.put(encrypted); String cookieValue = Base64.getEncoder().encodeToString(message.array()); context.addCookie( - Cookie.cookie(cookieName, cookieValue).setPath("/").setSameSite(cookieSameSite).setSecure(secureCookie) + Cookie.cookie(cookieName, cookieValue).setPath(cookiePath).setSameSite(cookieSameSite) + .setSecure(secureCookie) .setHttpOnly(httpOnlyCookie)); } catch (Exception e) { throw new RuntimeException(e);