Skip to content

Commit ded71f9

Browse files
authored
Merge pull request #104 from prime-framework/bhalsey/eng-3572/rfc-section-4
add DPoPProofProvider
2 parents 1cfaefd + e9a2360 commit ded71f9

5 files changed

Lines changed: 112 additions & 12 deletions

File tree

build.savant

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ logbackVersion = "1.5.13"
2929
slf4jVersion = "2.0.13"
3030
testngVersion = "7.8.0"
3131

32-
project(group: "org.primeframework", name: "prime-mvc", version: "5.8.1", licenses: ["ApacheV2_0"]) {
32+
project(group: "org.primeframework", name: "prime-mvc", version: "5.8.2", licenses: ["ApacheV2_0"]) {
3333
workflow {
3434
fetch {
3535
// Dependency resolution order:

src/test/java/org/example/action/HeaderValuesAction.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,9 @@ public String get() {
4545
headers.putAll(httpRequest.getHeaders());
4646
return "success";
4747
}
48+
49+
public String post() {
50+
headers.putAll(httpRequest.getHeaders());
51+
return "success";
52+
}
4853
}

src/test/java/org/primeframework/mvc/GlobalTest.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.primeframework.mvc;
1717

18+
import javax.annotation.Nullable;
1819
import java.io.ByteArrayInputStream;
1920
import java.io.File;
2021
import java.io.IOException;
@@ -49,6 +50,7 @@
4950
import com.google.inject.Key;
5051
import com.google.inject.TypeLiteral;
5152
import freemarker.template.Configuration;
53+
import io.fusionauth.http.HTTPMethod;
5254
import io.fusionauth.http.HTTPValues.Headers;
5355
import io.fusionauth.http.HTTPValues.Methods;
5456
import org.example.action.JwtAuthorizedAction;
@@ -72,6 +74,7 @@
7274
import org.primeframework.mvc.security.CBCCipherProvider;
7375
import org.primeframework.mvc.security.DefaultEncryptor;
7476
import org.primeframework.mvc.security.Encryptor;
77+
import org.primeframework.mvc.test.DPoPProofProvider;
7578
import org.primeframework.mvc.util.URIBuilder;
7679
import org.testng.annotations.BeforeClass;
7780
import org.testng.annotations.DataProvider;
@@ -1554,6 +1557,39 @@ public void headers() throws IOException {
15541557
.assertJSONValuesAt("/foo", List.of("bar", "baz"));
15551558
}
15561559

1560+
@Test
1561+
public void dpopHeader() throws IOException {
1562+
// Make sure DPoPProofProvider gets invoked with proper values
1563+
// (a real DPoPProofProvider would generate a signed JWT)
1564+
DPoPProofProvider provider = (httpMethod, htu, accessToken) -> httpMethod.toString() + ":" + htu + ":" + accessToken;
1565+
1566+
simulator.test("/header-values")
1567+
.withDPoPProofProvider(provider)
1568+
.get()
1569+
.assertStatusCode(200)
1570+
.assertJSONValuesAt("/dpop", List.of("GET:http://localhost:9080/header-values:null"));
1571+
1572+
simulator.test("/header-values")
1573+
.withDPoPProofProvider(provider)
1574+
.withAuthorizationBearerToken("fake.token")
1575+
.get()
1576+
.assertStatusCode(200)
1577+
.assertJSONValuesAt("/dpop", List.of("GET:http://localhost:9080/header-values:fake.token"));
1578+
1579+
simulator.test("/header-values")
1580+
.withDPoPProofProvider(provider)
1581+
.post()
1582+
.assertStatusCode(200)
1583+
.assertJSONValuesAt("/dpop", List.of("POST:http://localhost:9080/header-values:null"));
1584+
1585+
simulator.test("/header-values")
1586+
.withDPoPProofProvider(provider)
1587+
.withAuthorizationBearerToken("fake.token")
1588+
.post()
1589+
.assertStatusCode(200)
1590+
.assertJSONValuesAt("/dpop", List.of("POST:http://localhost:9080/header-values:fake.token"));
1591+
}
1592+
15571593
@Test
15581594
public void head() {
15591595
simulator.test("/head")
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright (c) 2025, Inversoft Inc., All Rights Reserved
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing,
11+
* software distributed under the License is distributed on an
12+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13+
* either express or implied. See the License for the specific
14+
* language governing permissions and limitations under the License.
15+
*/
16+
package org.primeframework.mvc.test;
17+
18+
import javax.annotation.Nullable;
19+
20+
import io.fusionauth.http.HTTPMethod;
21+
22+
/**
23+
* Interface for generating DPoP Proofs. Allows one to plug in their
24+
* own DPoP Proof generation to HTTP requests in a test framework.
25+
*/
26+
public interface DPoPProofProvider {
27+
/**
28+
* Generates a DPoP Proof.
29+
* @param httpMethod The HTTP Method. To be provided as the htm claim.
30+
* @param htu The HTTP URI. To be provided as the htu claim.
31+
* @param accessToken (Optional) The access token. To be provided as the hashed ath claim.
32+
* @return The DPoP Proof to be used in the DPoP header.
33+
*/
34+
String generateDPoPProof(HTTPMethod httpMethod, String htu, @Nullable String accessToken);
35+
}

src/test/java/org/primeframework/mvc/test/RequestBuilder.java

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454
import io.fusionauth.http.HTTPValues.Headers;
5555
import io.fusionauth.http.server.HTTPRequest;
5656
import io.fusionauth.http.server.HTTPResponse;
57+
import io.fusionauth.jwks.domain.JSONWebKey;
58+
import io.fusionauth.jwt.Signer;
5759
import org.primeframework.mock.MockUserAgent;
5860
import org.primeframework.mvc.config.MVCConfiguration;
5961
import org.primeframework.mvc.http.FormBodyPublisher;
@@ -94,6 +96,10 @@ public class RequestBuilder {
9496

9597
public boolean useTLS;
9698

99+
private String bearerToken;
100+
101+
private DPoPProofProvider dPoPProofProvider;
102+
97103
private byte[] body;
98104

99105
@Inject(optional = true)
@@ -328,6 +334,12 @@ public RequestBuilder usingHTTPS() {
328334
throw new IllegalStateException("This handling is not implemented yet");
329335
}
330336

337+
public RequestBuilder withAuthorizationBearerToken(String encodedJWT) {
338+
this.bearerToken = encodedJWT;
339+
request.setHeader("Authorization", "Bearer " + encodedJWT);
340+
return this;
341+
}
342+
331343
/**
332344
* Adds an Authorization header to the request using the specified value.
333345
* <p>Shorthand for calling
@@ -491,15 +503,9 @@ public RequestBuilder withCookie(String name, String value) throws Exception {
491503
return withCookie(name, value, false, false);
492504
}
493505

494-
/**
495-
* Encrypt the provided value and add a cookie with the encrypted value to the request
496-
*
497-
* @param name The name of the cookie.
498-
* @param value The unencrypted value of the cookie.
499-
* @return This.
500-
*/
501-
public RequestBuilder withEncryptedCookie(String name, String value) throws Exception {
502-
return withCookie(name, value, false, true);
506+
public RequestBuilder withDPoPProofProvider(DPoPProofProvider dPoPProofProvider) {
507+
this.dPoPProofProvider = dPoPProofProvider;
508+
return this;
503509
}
504510

505511
/**
@@ -526,6 +532,17 @@ public RequestBuilder withEncoding(Charset encoding) {
526532
return this;
527533
}
528534

535+
/**
536+
* Encrypt the provided value and add a cookie with the encrypted value to the request
537+
*
538+
* @param name The name of the cookie.
539+
* @param value The unencrypted value of the cookie.
540+
* @return This.
541+
*/
542+
public RequestBuilder withEncryptedCookie(String name, String value) throws Exception {
543+
return withCookie(name, value, false, true);
544+
}
545+
529546
/**
530547
* Adds a file.
531548
*
@@ -825,6 +842,10 @@ HTTPResponseWrapper run() {
825842
request.setHost(requestURI.getHost());
826843
request.setScheme(requestURI.getScheme());
827844

845+
if (dPoPProofProvider != null) {
846+
request.addHeader("DPoP", dPoPProofProvider.generateDPoPProof(request.getMethod(), requestURI.toString(), this.bearerToken));
847+
}
848+
828849
// Now that the cookies are ready, if the CSRF token is enabled and the parameter isn't set, we set it to be consistent
829850
// since the [@control.form] would normally set that into the form and into the request.
830851
if (request.getMethod() == HTTPMethod.POST) {
@@ -887,9 +908,12 @@ HTTPResponseWrapper run() {
887908
var requestBuilder = HttpRequest.newBuilder()
888909
.method(request.getMethod().name(), bodyPublisher);
889910

890-
if (!locales.isEmpty()) {
891-
requestBuilder.setHeader("Accept-Language", locales.stream().map(Locale::toLanguageTag).collect(Collectors.joining(", ")));
911+
if (locales.isEmpty()) {
912+
// request.getLocale() returns a default locale if none are set by httpRequestConsumer but
913+
// we still want to set to the system's default, if none were explicitly set.
914+
locales = List.of(Locale.getDefault());
892915
}
916+
requestBuilder.setHeader("Accept-Language", locales.stream().map(Locale::toLanguageTag).collect(Collectors.joining(", ")));
893917

894918
if (contentType != null) {
895919
requestBuilder.setHeader(Headers.ContentType, contentType + (characterEncoding != null ? "; charset=" + characterEncoding : ""));

0 commit comments

Comments
 (0)