Skip to content

Commit b919bbb

Browse files
authored
fix: JWKS Request should not be duplicated headers added by filters (#1924)
Close: #1881
1 parent 6455bd8 commit b919bbb

File tree

3 files changed

+124
-5
lines changed

3 files changed

+124
-5
lines changed

security-jwt/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ dependencies {
1515
implementation(mnReactor.micronaut.reactor)
1616
compileOnly(mnCache.micronaut.cache.core)
1717
testImplementation(libs.bcpkix.jdk15on)
18+
testImplementation(mnCache.micronaut.cache.caffeine)
1819

1920
compileOnly(mn.micronaut.http.client.core)
2021
compileOnly(mn.micronaut.http.server)
@@ -38,6 +39,10 @@ dependencies {
3839
testImplementation(mnTestResources.testcontainers.core)
3940

4041
testImplementation(libs.system.stubs.core)
42+
43+
testImplementation(mnTest.micronaut.test.junit5)
44+
testImplementation(libs.junit.jupiter.params)
45+
testRuntimeOnly(libs.junit.jupiter.engine)
4146
}
4247

4348
tasks.test {

security-jwt/src/main/java/io/micronaut/security/token/jwt/signature/jwks/ReactorCacheJwkSetFetcher.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,18 +62,18 @@ public Publisher<JWKSet> fetch(@Nullable String providerName, @Nullable String u
6262
}
6363

6464
private Mono<JwksCacheEntry> jwksCacheEntry(CacheKey cacheKey) {
65-
return Mono.from(super.fetch(cacheKey.providerName, cacheKey.url()))
66-
.defaultIfEmpty(new JWKSet())
67-
.map(jwksSet -> instantiateCacheEntry(cacheKey, jwksSet))
68-
.cacheInvalidateIf(JwksCacheEntry::isExpired);
65+
return Mono.defer(() -> Mono.from(super.fetch(cacheKey.providerName, cacheKey.url())))
66+
.defaultIfEmpty(new JWKSet())
67+
.map(jwksSet -> instantiateCacheEntry(cacheKey, jwksSet))
68+
.cacheInvalidateIf(JwksCacheEntry::isExpired);
6969
}
7070

7171
private JwksCacheEntry instantiateCacheEntry(CacheKey cacheKey, JWKSet jwkSet) {
7272
return new JwksCacheEntry(jwkSet, Instant.now().plusSeconds(jwksSignatureConfigurations.get(cacheKey.providerName) != null
7373
? jwksSignatureConfigurations.get(cacheKey.providerName).getCacheExpiration()
7474
: JwksSignatureConfigurationProperties.DEFAULT_CACHE_EXPIRATION));
7575
}
76-
76+
7777
private record CacheKey(String providerName, String url) {
7878
}
7979

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package io.micronaut.security.token.jwt.signature.jwks;
2+
3+
import io.micronaut.context.ApplicationContext;
4+
import io.micronaut.context.annotation.Requires;
5+
import io.micronaut.http.HttpRequest;
6+
import io.micronaut.http.HttpResponse;
7+
import io.micronaut.http.MutableHttpRequest;
8+
import io.micronaut.http.annotation.Controller;
9+
import io.micronaut.http.annotation.Filter;
10+
import io.micronaut.http.annotation.Get;
11+
import io.micronaut.http.client.BlockingHttpClient;
12+
import io.micronaut.http.client.HttpClient;
13+
import io.micronaut.http.filter.ClientFilterChain;
14+
import io.micronaut.http.filter.HttpClientFilter;
15+
import io.micronaut.runtime.server.EmbeddedServer;
16+
import io.micronaut.security.annotation.Secured;
17+
import io.micronaut.security.rules.SecurityRule;
18+
import org.junit.jupiter.params.ParameterizedTest;
19+
import org.junit.jupiter.params.provider.Arguments;
20+
import org.junit.jupiter.params.provider.MethodSource;
21+
import org.reactivestreams.Publisher;
22+
import java.util.ArrayList;
23+
import java.util.Collections;
24+
import java.util.List;
25+
import java.util.Map;
26+
import java.util.stream.Stream;
27+
28+
import static org.junit.jupiter.api.Assertions.*;
29+
import static org.junit.jupiter.params.provider.Arguments.arguments;
30+
31+
class JwksRequestDoesNotDuplicateHeadersTest {
32+
static Stream<Arguments> paramsProvider() {
33+
return Stream.of(
34+
arguments(CacheableJwkSetFetcher.class, "micronaut.caches.jwks.expire-after-write", "0s"),
35+
arguments(ReactorCacheJwkSetFetcher.class, "micronaut.security.token.jwt.signatures.jwks.google.cache-expiration", "0"));
36+
}
37+
38+
@ParameterizedTest
39+
@MethodSource("paramsProvider")
40+
void jwksRequestDoesNotDuplicateHeadersTest(Class fetcherClass, String configName, String configValue) {
41+
Map<String, Object> authServerConfig = Map.of("spec.name", "JwksRequestDoesNotDuplicateHeadersTestGoogle");
42+
try (EmbeddedServer authServer = ApplicationContext.run(EmbeddedServer.class, authServerConfig)) {
43+
Map<String, Object> serverConfig = Map.of("spec.name", "JwksRequestDoesNotDuplicateHeadersTest",
44+
"micronaut.http.client.read-timeout", "5m",
45+
"micronaut.security.token.jwt.signatures.jwks.google.url",
46+
"http://localhost:" + authServer.getPort() + "/oauth2/v3/certs",
47+
configName, configValue);
48+
try (EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class, serverConfig)) {
49+
assertInstanceOf(fetcherClass, server.getApplicationContext().getBean(JwkSetFetcher.class));
50+
HttpClient httpClient = server.getApplicationContext().createBean(HttpClient.class, server.getURL());
51+
BlockingHttpClient client = httpClient.toBlocking();
52+
assertNotNull(client);
53+
String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
54+
int numberOfRequests = 5;
55+
for (int i = 0; i < numberOfRequests; i++) {
56+
assertEquals("{\"message\":\"Hello World\"}", client.retrieve(HttpRequest.GET("/").bearerAuth(jwt)));
57+
}
58+
assertEquals(5, authServer.getApplicationContext().getBean(GoogleAuthController.class).headers.size());
59+
}
60+
}
61+
}
62+
63+
@Requires(property = "spec.name", value = "JwksRequestDoesNotDuplicateHeadersTest")
64+
@Controller
65+
static class HomeController {
66+
67+
@Secured(SecurityRule.IS_ANONYMOUS)
68+
@Get
69+
Map<String, String> index() {
70+
return Collections.singletonMap("message", "Hello World");
71+
}
72+
}
73+
74+
@Requires(property = "spec.name", value = "JwksRequestDoesNotDuplicateHeadersTest")
75+
@Filter(patterns = "/oauth2/v3/certs")
76+
static class AddHeaderFilter implements HttpClientFilter {
77+
public Publisher<? extends HttpResponse<?>> doFilter(MutableHttpRequest<?> request, ClientFilterChain chain) {
78+
return chain.proceed(request.header("hdr", "a really long string ..."));
79+
}
80+
}
81+
82+
@Requires(property = "spec.name", value = "JwksRequestDoesNotDuplicateHeadersTestGoogle")
83+
@Controller
84+
static class GoogleAuthController {
85+
List<String> headers = new ArrayList<>();
86+
@Secured(SecurityRule.IS_ANONYMOUS)
87+
@Get("/oauth2/v3/certs")
88+
String index(HttpRequest<?> request) {
89+
headers.addAll(request.getHeaders().getAll("hdr"));
90+
return """
91+
{
92+
"keys": [
93+
{
94+
"kty": "RSA",
95+
"e": "AQAB",
96+
"use": "sig",
97+
"kid": "fa072f75784642615087c7182c101341e18f7a3a",
98+
"n": "pleuF0RyDsETygZn89RpGVFNMxG_hdYVnvbHadvM1tYxs9ghDq93NFxejt--1QlwpLQ3yuVY_CKldkAWgzPVl8-oUBe5xh9jzpLUTqcyrS1aFLuzAe13-OTadUE18wvhz9goQf80rg5IztD_gBePOOBE7eWHGqWLghuMb7cIYjgFxqNFyPn8bF_7k8pQAeHIPua_6_GHhw3ML4msp-aU7O1io3Z4P_Bir_6_C5J9UtWAcJ0Ez0YC5FxOMkh27joO5mUas8krGnFqIJTOgDYXQC1QTu-HOCRNvi6gFMqEkDTP5oBK2cDPDq5L0T8Q0UanSPR0BuOTHesCXnDAdxdyXw",
99+
"alg": "RS256"
100+
},
101+
{
102+
"n": "5D9Xb4z8eFr-3Zh3m5GnM_KVqc6rskPL7EMa6lSxNiMJ-PhXGORU-S-QgLmMvHu3vAMfvxz6ph3JZDpdGT68wj-vWqqBudaDYCbnbkjXm6UpcrFMpGAiOS6gACNxpz80JXaO2DPtl9jTN6WyJY9tLHdqRfesfOlwzB0lmVZ8shSDh8usN3vB1KfYuR6Vytly1phaWJr92yMICKUjtXT-0SlrtqDgX_U2Swl4QyZN6rrfuG3F6Fmw-m12Ve_kyoPUb02bbJCSFDnIZsMvRlSZem5nUrs86zDPTWfNcB0LUYG8OgMzOev7r04h_RY2F6K7c8nE2EobYTrH0kw2QIf8vQ",
103+
"alg": "RS256",
104+
"kid": "eec534fa5b8caca201ca8d0ff96b54c562210d1e",
105+
"e": "AQAB",
106+
"kty": "RSA",
107+
"use": "sig"
108+
}
109+
]
110+
}
111+
""";
112+
}
113+
}
114+
}

0 commit comments

Comments
 (0)