Skip to content

Commit

Permalink
Path configuration for form and webauthn cookies
Browse files Browse the repository at this point in the history
  • Loading branch information
sberyozkin committed Jan 17, 2023
1 parent e36686a commit 47bb512
Show file tree
Hide file tree
Showing 9 changed files with 227 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String> cookiePath = Optional.of("/");
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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"));

}
}
Original file line number Diff line number Diff line change
@@ -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"));

}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<String> cookiePath = Optional.of("/");

/**
* Set the HttpOnly attribute to prevent access to the cookie via JavaScript.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}

Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,17 +266,20 @@ 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;
String postLocation = form.postLocation.startsWith("/") ? form.postLocation : "/" + form.postLocation;
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);
}
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 47bb512

Please sign in to comment.