Skip to content

Commit

Permalink
Merge pull request #69 from prime-framework/degroff/referer_handler
Browse files Browse the repository at this point in the history
Changes to the Request Builder to allow for additional flexibility when using the `Referer` header.
  • Loading branch information
robotdan authored Jan 23, 2025
2 parents 95ee8cf + 9f81e22 commit c0b3543
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 16 deletions.
4 changes: 2 additions & 2 deletions build.savant
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2014-2024, Inversoft Inc., All Rights Reserved
* Copyright (c) 2014-2025, Inversoft Inc., All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -29,7 +29,7 @@ logbackVersion = "1.4.14"
slf4jVersion = "2.0.13"
testngVersion = "7.8.0"

project(group: "org.primeframework", name: "prime-mvc", version: "4.28.0", licenses: ["ApacheV2_0"]) {
project(group: "org.primeframework", name: "prime-mvc", version: "4.29.0", licenses: ["ApacheV2_0"]) {
workflow {
fetch {
// Dependency resolution order:
Expand Down
8 changes: 7 additions & 1 deletion src/test/java/org/example/action/SecureAction.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2015-2017, Inversoft Inc., All Rights Reserved
* Copyright (c) 2015-2025, Inversoft Inc., All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -15,11 +15,14 @@
*/
package org.example.action;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import io.fusionauth.http.HTTPValues.Methods;
import org.primeframework.mvc.action.annotation.Action;
import org.primeframework.mvc.action.result.annotation.Status;
import org.primeframework.mvc.parameter.annotation.UnknownParameters;
import org.primeframework.mvc.security.annotation.ConstraintOverride;
import org.primeframework.mvc.security.annotation.ConstraintOverrideMethod;

Expand All @@ -34,6 +37,9 @@
@Status(code = "unauthorized", status = 403)
})
public class SecureAction {
@UnknownParameters
public static Map<String, String[]> UnknownParameters = new HashMap<>();

@ConstraintOverrideMethod(httpMethods = {Methods.PATCH})
public List<String> customConstraintsForPatch() {
return List.of("patch-only");
Expand Down
27 changes: 25 additions & 2 deletions src/test/java/org/primeframework/mvc/CSRFTest.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2019-2024, Inversoft Inc., All Rights Reserved
* Copyright (c) 2019-2025, Inversoft Inc., All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -18,21 +18,40 @@
import java.util.Base64;

import com.google.inject.Inject;
import org.example.action.SecureAction;
import org.example.domain.User;
import org.primeframework.mvc.security.CBCCipherProvider;
import org.primeframework.mvc.security.DefaultEncryptor;
import org.primeframework.mvc.security.Encryptor;
import org.primeframework.mvc.security.MockUserLoginSecurityContext;
import org.primeframework.mvc.security.UserLoginSecurityContext;
import org.testng.annotations.Test;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;

/**
* @author Daniel DeGroff
*/
public class CSRFTest extends PrimeBaseTest {
@Inject public UserLoginSecurityContext securityContext;

@Test(enabled = false)
@Test
public void get_CSRFToken() {
MockUserLoginSecurityContext.roles.add("admin");
securityContext.login(new User());

configuration.csrfEnabled = true;
simulator.test("/secure")
.get()
.assertStatusCode(200)
.assertBody("Secure!");

// A GET request won't contain the parameter
// - This is just testing the RequestBuilder that will try and automatically add the CSRF token
assertFalse(SecureAction.UnknownParameters.containsKey(csrfProvider.getParameterName()));
}

@Test
public void post_CSRFOriginFailure() {
MockUserLoginSecurityContext.roles.add("admin");
securityContext.login(new User());
Expand Down Expand Up @@ -122,6 +141,10 @@ public void post_CSRFTokenCompatibility() throws Exception {
.post()
.assertStatusCode(200)
.assertBody("Secure!");

// A POST request will contain the CSRF token
// - This is just testing the RequestBuilder that will try and automatically add the CSRF token
assertTrue(SecureAction.UnknownParameters.containsKey(csrfProvider.getParameterName()));
}

// Add for testing legacy-encrypted CSRF token which is defined as a private class in DefaultEncryptionBasedTokenCSRFProvider
Expand Down
11 changes: 10 additions & 1 deletion src/test/java/org/primeframework/mvc/GlobalTest.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2001-2024, Inversoft Inc., All Rights Reserved
* Copyright (c) 2001-2025, Inversoft Inc., All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -814,6 +814,15 @@ public void get_metricsErrors() {
assertEquals(meters.get("prime-mvc.[*].errors").getCount(), 1);
}

@Test
public void get_modifyRequest() throws Exception {
// The Test HTTP request consumer will have added an HTTP request header.
simulator.test("/foo")
.get()
.assertStatusCode(200)
.custom(result -> assertEquals(result.request.getHeader("X-Test-HTTP-Request-Consumer"), "true"));
}

@Test
public void get_nested_parameters() throws Exception {
test.simulate(() -> simulator.test("/nested")
Expand Down
15 changes: 14 additions & 1 deletion src/test/java/org/primeframework/mvc/PrimeBaseTest.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2012-2024, Inversoft Inc., All Rights Reserved
* Copyright (c) 2012-2025, Inversoft Inc., All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -51,6 +51,7 @@
import io.fusionauth.http.server.HTTPRequest;
import io.fusionauth.http.server.HTTPResponse;
import io.fusionauth.http.server.HTTPServerConfiguration;
import org.example.action.SecureAction;
import org.example.action.user.EditAction;
import org.primeframework.mvc.action.ActionInvocation;
import org.primeframework.mvc.action.ExecuteMethodConfiguration;
Expand Down Expand Up @@ -85,6 +86,7 @@
import org.primeframework.mvc.security.UserLoginSecurityContext;
import org.primeframework.mvc.security.VerifierProvider;
import org.primeframework.mvc.security.csrf.CSRFProvider;
import org.primeframework.mvc.test.RequestBuilder.HTTPRequestConsumer;
import org.primeframework.mvc.test.RequestSimulator;
import org.primeframework.mvc.util.ThrowingRunnable;
import org.primeframework.mvc.validation.Validation;
Expand Down Expand Up @@ -214,6 +216,7 @@ public void beforeMethod() {

// Reset
EditAction.getCalled = false;
SecureAction.UnknownParameters.clear();

TestUnhandledExceptionHandler.reset();
}
Expand All @@ -226,6 +229,7 @@ protected void configure() {
super.configure();
install(new TestMVCConfigurationModule());
bind(CORSConfigurationProvider.class).to(TestCORSConfigurationProvider.class).in(Singleton.class);
bind(HTTPRequestConsumer.class).to(TestHTTPRequestConsumer.class);
bind(MessageObserver.class).toInstance(messageObserver);
bind(MetricRegistry.class).toInstance(metricRegistry);
bind(UserLoginSecurityContext.class).to(MockUserLoginSecurityContext.class);
Expand Down Expand Up @@ -342,6 +346,15 @@ protected void configure() {
}
}

public static class TestHTTPRequestConsumer implements HTTPRequestConsumer {
/**
* @param httpRequest the http request
*/
public void accept(HTTPRequest httpRequest) {
httpRequest.setHeader("X-Test-HTTP-Request-Consumer", "true");
}
}

@SuppressWarnings("unused")
public static class TestListener implements ITestListener {
private final static AtomicInteger TestCounter = new AtomicInteger(0);
Expand Down
38 changes: 29 additions & 9 deletions src/test/java/org/primeframework/mvc/test/RequestBuilder.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2012-2024, Inversoft Inc., All Rights Reserved
* Copyright (c) 2012-2025, Inversoft Inc., All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -45,6 +45,7 @@

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.inject.Inject;
import com.google.inject.Injector;
import io.fusionauth.http.Cookie;
import io.fusionauth.http.Cookie.SameSite;
Expand Down Expand Up @@ -96,6 +97,9 @@ public class RequestBuilder {

private byte[] body;

@Inject(optional = true)
private HTTPRequestConsumer httpRequestConsumer;

public RequestBuilder(String path, Injector injector, MockUserAgent userAgent, TestMessageObserver messageObserver,
int port) {
this.injector = injector;
Expand All @@ -104,6 +108,8 @@ public RequestBuilder(String path, Injector injector, MockUserAgent userAgent, T
this.request = new HTTPRequest().with(r -> r.addLocales(Locale.US))
.with(r -> r.setPath(path));
this.port = port;
//Injections optionally HTTPRequestConsumer
injector.injectMembers(this);
}

public static HttpClient newHttpClient() {
Expand Down Expand Up @@ -832,18 +838,25 @@ HTTPResponseWrapper run() {

// Now that the cookies are ready, if the CSRF token is enabled and the parameter isn't set, we set it to be consistent
// since the [@control.form] would normally set that into the form and into the request.
MVCConfiguration configuration = injector.getInstance(MVCConfiguration.class);
if (configuration.csrfEnabled()) {
CSRFProvider csrfProvider = injector.getInstance(CSRFProvider.class);
if (csrfProvider.getTokenFromRequest(request) == null) {
String token = csrfProvider.getToken(request);
if (token != null) {
String parameterName = csrfProvider.getParameterName();
request.addURLParameter(parameterName, token);
if (request.getMethod() == HTTPMethod.POST) {
MVCConfiguration configuration = injector.getInstance(MVCConfiguration.class);
if (configuration.csrfEnabled()) {
CSRFProvider csrfProvider = injector.getInstance(CSRFProvider.class);
if (csrfProvider.getTokenFromRequest(request) == null) {
String token = csrfProvider.getToken(request);
if (token != null) {
String parameterName = csrfProvider.getParameterName();
requestBodyParameters.put(parameterName, List.of(token));
}
}
}
}

// Allow the caller to consume the HTTP request in order to mutate it and what not.
if (httpRequestConsumer != null) {
httpRequestConsumer.accept(request);
}

List<Locale> locales = request.getLocales();
String contentType = request.getContentType();
Charset characterEncoding = request.getCharacterEncoding();
Expand Down Expand Up @@ -965,4 +978,11 @@ private void setRequestBodyParameter(String name, Object value) {
values.add(value.toString());
requestBodyParameters.put(name, values);
}

public interface HTTPRequestConsumer {
/**
* @param httpRequest the http request
*/
void accept(HTTPRequest httpRequest);
}
}

0 comments on commit c0b3543

Please sign in to comment.