Skip to content

Commit

Permalink
Apply HTTP Security Policy on normalized path
Browse files Browse the repository at this point in the history
- Use normalized path to check Keycloak authorization policies
- Use normalised path for Servlet
- Use normalized path to check CSRF token path constraint

Co-authored-by: Michal Vavrik <mvavrik@redhat.com>
Co-authored-by: Stuart Douglas <stuart.w.douglas@gmail.com>
Co-authored-by: Sergey Beryozkin <sberyozkin@gmail.com>
Co-authored-by: Clement Escoffier <clement.escoffier@gmail.com>
  • Loading branch information
4 people committed Sep 13, 2023
1 parent 35b6bd8 commit a3694e0
Show file tree
Hide file tree
Showing 16 changed files with 934 additions and 162 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@ private String getCookieToken(RoutingContext routing, CsrfReactiveConfig config)
}

private boolean isCsrfTokenRequired(RoutingContext routing, CsrfReactiveConfig config) {
return config.createTokenPath.isPresent() ? config.createTokenPath.get().contains(routing.request().path()) : true;
return config.createTokenPath
.map(value -> value.contains(routing.normalizedPath())).orElse(true);
}

private void createCookie(String csrfToken, RoutingContext routing, CsrfReactiveConfig config) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ public Uni<CheckResult> checkPermission(RoutingContext request, Uni<SecurityIden
public CheckResult apply(RoutingContext routingContext, SecurityIdentity identity) {

if (identity.isAnonymous()) {
PathConfig pathConfig = resolver.getPolicyEnforcer(null).getPathMatcher().matches(routingContext.request().path());
PathConfig pathConfig = resolver.getPolicyEnforcer(null).getPathMatcher().matches(
routingContext.normalizedPath());
if (pathConfig != null && pathConfig.getEnforcementMode() == EnforcementMode.ENFORCING) {
return CheckResult.DENY;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.URI;
import java.util.List;

import org.keycloak.adapters.authorization.TokenPrincipal;
Expand Down Expand Up @@ -122,7 +121,7 @@ public String getURI() {

@Override
public String getRelativePath() {
return URI.create(request.uri()).getPath();
return routingContext.normalizedPath();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
package io.quarkus.resteasy.test.security;

import static org.apache.commons.codec.binary.Base64.encodeBase64URLSafeString;
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.net.URL;
import java.time.Duration;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import io.quarkus.security.test.utils.TestIdentityController;
import io.quarkus.security.test.utils.TestIdentityProvider;
import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.test.common.http.TestHTTPResource;
import io.smallrye.mutiny.Uni;
import io.vertx.core.http.HttpMethod;
import io.vertx.mutiny.core.Vertx;
import io.vertx.mutiny.core.http.HttpClientRequest;

public class JakartaRestResourceHttpPermissionTest {
private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(20);
private static final String APP_PROPS = "quarkus.http.auth.permission.foo.paths=/api/foo,/api/foo/\n" +
"quarkus.http.auth.permission.foo.policy=authenticated\n" +
"quarkus.http.auth.permission.bar.paths=api/bar*\n" +
"quarkus.http.auth.permission.bar.policy=authenticated\n" +
"quarkus.http.auth.permission.baz-fum-pub.paths=/api/baz/fum\n" +
"quarkus.http.auth.permission.baz-fum-pub.policy=permit\n" +
"quarkus.http.auth.permission.baz-fum.paths=/api/baz/fum*\n" +
"quarkus.http.auth.permission.baz-fum.policy=authenticated\n" +
"quarkus.http.auth.permission.root.paths=/\n" +
"quarkus.http.auth.permission.root.policy=authenticated\n" +
"quarkus.http.auth.permission.dot.paths=dot,dot/\n" +
"quarkus.http.auth.permission.dot.policy=authenticated\n";

@RegisterExtension
static QuarkusUnitTest runner = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(TestIdentityProvider.class, TestIdentityController.class, ApiResource.class,
RootResource.class, PublicResource.class)
.addAsResource(new StringAsset(APP_PROPS), "application.properties"));

@BeforeAll
public static void setup() {
TestIdentityController.resetRoles().add("test", "test", "test");
}

@TestHTTPResource
URL url;

@Inject
Vertx vertx;

@ParameterizedTest
@ValueSource(strings = {
// path without wildcard, with leading slashes in both policy and @Path
"/api/foo/", "/api/foo",
// path with wildcard, without leading slashes in both policy and @Path
"/api/bar", "/api/bar/", "/api/bar/irish",
// combination of permit and authenticated policies, paths are resolved to /api/baz/fum/ and auth required
"/api/baz/fum/"
})
public void testEmptyPathSegments(String path) {
assurePath(path, 401);
assurePathAuthenticated(path, getLastNonEmptySegmentContent(path));
}

@ParameterizedTest
@ValueSource(strings = { "/", "///", "/?stuff", "" })
public void testRootPath(String path) {
assurePath(path, 401);
assurePathAuthenticated(path);
}

@ParameterizedTest
@ValueSource(strings = { "/one/", "/three?stuff" })
public void testNotSecuredPaths(String path) {
// negative testing - all paths are public unless auth policy is applied
assurePath(path, 200);
}

@ParameterizedTest
@ValueSource(strings = { "/api/foo///", "////api/foo", "/api//foo", "/api/bar///irish", "/api/bar///irish/",
"/api//baz/fum//",
"/api///foo", "////api/bar", "/api///bar", "/api//bar" })
public void testSecuredNotFound(String path) {
assurePath(path, 401);
assurePathAuthenticated(path, 404);
}

private static String getLastNonEmptySegmentContent(String path) {
while (path.endsWith("/") || path.endsWith(".")) {
path = path.substring(0, path.length() - 1);
}
return path.substring(path.lastIndexOf('/') + 1);
}

@Path("/api")
public static class ApiResource {

@GET
@Path("/foo")
public String foo() {
return "foo";
}

@GET
@Path("/bar")
public String bar() {
return "bar";
}

@GET
@Path("/bar/irish")
public String irishBar() {
return "irish";
}

@GET
@Path("/baz/fum")
public String bazFum() {
return "fum";
}

}

@Path("/")
public static class RootResource {

@GET
public String get() {
return "root";
}

}

@Path("/")
public static class PublicResource {

@Path("one")
@GET
public String one() {
return "one";
}

@Path("/two")
@GET
public String two() {
return "two";
}

@Path("/three")
@GET
public String three() {
return "three";
}

@Path("four")
@GET
public String four() {
return "four";
}

@Path("/four#stuff")
@GET
public String fourWitFragment() {
return "four#stuff";
}

@Path("five")
@GET
public String five() {
return "five";
}

}

private void assurePath(String path, int expectedStatusCode) {
assurePath(path, expectedStatusCode, null, false);
}

private void assurePathAuthenticated(String path) {
assurePath(path, 200, null, true);
}

private void assurePathAuthenticated(String path, int statusCode) {
assurePath(path, statusCode, null, true);
}

private void assurePathAuthenticated(String path, String body) {
assurePath(path, 200, body, true);
}

private void assurePath(String path, int expectedStatusCode, String body, boolean auth) {
var httpClient = vertx.createHttpClient();
try {
httpClient
.request(HttpMethod.GET, url.getPort(), url.getHost(), path)
.map(r -> {
if (auth) {
r.putHeader("Authorization", "Basic " + encodeBase64URLSafeString("test:test".getBytes()));
}
return r;
})
.flatMap(HttpClientRequest::send)
.invoke(r -> assertEquals(expectedStatusCode, r.statusCode(), path))
.flatMap(r -> {
if (body != null) {
return r.body().invoke(b -> assertEquals(b.toString(), body, path));
} else {
return Uni.createFrom().nullItem();
}
})
.await()
.atMost(REQUEST_TIMEOUT);
} finally {
httpClient
.close()
.await()
.atMost(REQUEST_TIMEOUT);
}
}
}
Loading

0 comments on commit a3694e0

Please sign in to comment.