Skip to content

Commit aa6f6ba

Browse files
committed
Refactoring and testing.
1 parent df61330 commit aa6f6ba

File tree

15 files changed

+1024
-61
lines changed

15 files changed

+1024
-61
lines changed

gradle/libs.versions.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ groovy = "4.0.13"
88

99
geb = "7.0"
1010
selenium = "4.15.0"
11+
system-stubs-core = "2.1.4"
1112
testcontainers = "1.19.2"
1213
unboundid-ldapsdk = "6.0.10"
1314
bouncycastle = "1.70"
@@ -50,6 +51,8 @@ bcpkix-jdk15on = { module = "org.bouncycastle:bcpkix-jdk15on", version.ref = "bo
5051
bcprov-jdk15on = { module = "org.bouncycastle:bcprov-jdk15on", version.ref = "bouncycastle" }
5152
kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" }
5253
bcpkix = { module = "org.bouncycastle:bcpkix-jdk15on", version.ref = "bcpkix" }
54+
system-stubs-core = { module = "uk.org.webcompere:system-stubs-core", version.ref = "system-stubs-core" }
55+
5356

5457
testcontainers-bom = { module = "org.testcontainers:testcontainers-bom", version.ref = "testcontainers" }
5558
testcontainers = { module = "org.testcontainers:testcontainers" }

security-jwt/build.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ dependencies {
88
annotationProcessor(mnValidation.micronaut.validation.processor)
99
api(mnValidation.micronaut.validation)
1010
api(projects.micronautSecurity)
11-
implementation(mn.micronaut.http.client.core)
1211
api(libs.managed.nimbus.jose.jwt)
1312
implementation(mnReactor.micronaut.reactor)
1413
testImplementation(libs.bcpkix.jdk15on)
1514
testImplementation(libs.bcprov.jdk15on)
1615

16+
compileOnly(mn.micronaut.http.client.core)
1717
compileOnly(mn.micronaut.http.server)
1818
compileOnly(mn.micronaut.json.core)
1919

@@ -35,6 +35,8 @@ dependencies {
3535

3636
testImplementation(platform(libs.testcontainers.bom))
3737
testImplementation(libs.testcontainers)
38+
39+
testImplementation(libs.system.stubs.core)
3840
}
3941

4042
test {

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

Lines changed: 10 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -16,30 +16,18 @@
1616
package io.micronaut.security.token.jwt.signature.jwks;
1717

1818
import com.nimbusds.jose.jwk.JWKSet;
19-
import io.micronaut.context.BeanContext;
2019
import io.micronaut.core.annotation.Blocking;
2120
import io.micronaut.core.annotation.NonNull;
2221
import io.micronaut.core.annotation.Nullable;
2322
import io.micronaut.core.optim.StaticOptimizations;
24-
import io.micronaut.core.util.SupplierUtil;
25-
import io.micronaut.http.client.HttpClient;
26-
import io.micronaut.http.client.HttpClientConfiguration;
27-
import io.micronaut.http.client.LoadBalancer;
28-
import io.micronaut.http.client.exceptions.HttpClientException;
29-
import io.micronaut.inject.qualifiers.Qualifiers;
3023
import jakarta.inject.Singleton;
3124
import org.slf4j.Logger;
3225
import org.slf4j.LoggerFactory;
33-
import reactor.core.publisher.Mono;
3426

35-
import java.io.IOException;
36-
import java.net.URL;
3727
import java.text.ParseException;
3828
import java.util.Collections;
3929
import java.util.Map;
40-
import java.util.Objects;
4130
import java.util.Optional;
42-
import java.util.concurrent.ConcurrentHashMap;
4331
import java.util.function.Supplier;
4432

4533
/**
@@ -53,14 +41,10 @@ public class DefaultJwkSetFetcher implements JwkSetFetcher<JWKSet> {
5341

5442
private static final Logger LOG = LoggerFactory.getLogger(DefaultJwkSetFetcher.class);
5543

56-
private final BeanContext beanContext;
57-
private final Supplier<HttpClient> defaultJwkSetClient;
58-
private final ConcurrentHashMap<String, HttpClient> jwkSetClients = new ConcurrentHashMap<>();
44+
private final JwksClient jwksClient;
5945

60-
public DefaultJwkSetFetcher(BeanContext beanContext,
61-
HttpClientConfiguration defaultClientConfiguration) {
62-
this.beanContext = beanContext;
63-
this.defaultJwkSetClient = SupplierUtil.memoized(() -> beanContext.createBean(HttpClient.class, LoadBalancer.empty(), defaultClientConfiguration));
46+
public DefaultJwkSetFetcher(JwksClient jwksClient) {
47+
this.jwksClient = jwksClient;
6448
}
6549

6650
@Override
@@ -72,13 +56,13 @@ public Optional<JWKSet> fetch(@Nullable String url) {
7256
}
7357
return OPTIMIZATIONS.findJwkSet(url)
7458
.map(s -> Optional.of(s.get()))
75-
.orElseGet(() -> Optional.ofNullable(load(url)));
59+
.orElseGet(() -> Optional.ofNullable(load(null, url)));
7660
}
7761

7862
@Override
7963
@NonNull
8064
@Blocking
81-
public Optional<JWKSet> fetch(@NonNull String providerName, @Nullable String url) {
65+
public Optional<JWKSet> fetch(@Nullable String providerName, @Nullable String url) {
8266
if (url == null) {
8367
return Optional.empty();
8468
}
@@ -93,44 +77,18 @@ public void clearCache(@NonNull String url) {
9377
}
9478

9579
@Nullable
96-
private JWKSet load(@NonNull String url) {
80+
private JWKSet load(@Nullable String providerName, @NonNull String url) {
9781
try {
98-
return JWKSet.load(new URL(url));
99-
} catch (IOException | ParseException e) {
82+
String jwkSetContent = jwksClient.load(providerName, url);
83+
return jwkSetContent != null ? JWKSet.parse(jwkSetContent) : null;
84+
} catch (ParseException e) {
10085
if (LOG.isErrorEnabled()) {
101-
LOG.error("Exception loading JWK from " + url, e);
86+
LOG.error("Exception parsing JWK Set response from " + url, e);
10287
}
10388
}
10489
return null;
10590
}
10691

107-
@Nullable
108-
private JWKSet load(@NonNull String providerName, @NonNull String url) {
109-
try {
110-
String jwkSetContent = Mono.from(getClient(providerName).retrieve(url)).block();
111-
Objects.requireNonNull(jwkSetContent, "JWK Set must not be null.");
112-
return JWKSet.parse(jwkSetContent);
113-
} catch (HttpClientException | ParseException e) {
114-
if (LOG.isErrorEnabled()) {
115-
LOG.error("Exception loading JWK from " + url, e);
116-
}
117-
}
118-
return null;
119-
}
120-
121-
/**
122-
* Retrieves a client for the given provider.
123-
*
124-
* @param providerName The provider name
125-
* @return An HTTP client to use to send the request
126-
*/
127-
protected HttpClient getClient(String providerName) {
128-
return jwkSetClients.computeIfAbsent(providerName, provider -> {
129-
Optional<io.micronaut.http.client.HttpClient> client = beanContext.findBean(io.micronaut.http.client.HttpClient.class, Qualifiers.byName(provider));
130-
return client.orElseGet(defaultJwkSetClient);
131-
});
132-
}
133-
13492
/**
13593
* AOT Optimizations.
13694
*/
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright 2017-2023 original authors
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+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.micronaut.security.token.jwt.signature.jwks;
17+
18+
import io.micronaut.context.BeanContext;
19+
import io.micronaut.context.annotation.Primary;
20+
import io.micronaut.context.annotation.Requires;
21+
import io.micronaut.core.annotation.NonNull;
22+
import io.micronaut.core.annotation.Nullable;
23+
import io.micronaut.core.util.SupplierUtil;
24+
import io.micronaut.http.client.HttpClient;
25+
import io.micronaut.http.client.HttpClientConfiguration;
26+
import io.micronaut.http.client.HttpClientRegistry;
27+
import io.micronaut.http.client.HttpVersionSelection;
28+
import io.micronaut.http.client.LoadBalancer;
29+
import io.micronaut.http.client.ServiceHttpClientConfiguration;
30+
import io.micronaut.http.client.exceptions.HttpClientException;
31+
import io.micronaut.inject.qualifiers.Qualifiers;
32+
import jakarta.inject.Singleton;
33+
import org.slf4j.Logger;
34+
import org.slf4j.LoggerFactory;
35+
import reactor.core.publisher.Mono;
36+
37+
import java.util.concurrent.ConcurrentHashMap;
38+
import java.util.function.Supplier;
39+
40+
/**
41+
* Implementation of {@link JwksClient} that uses the Micronaut {@link HttpClient}.
42+
*
43+
* <p>
44+
* If a named service-specific client is configured (i.e. with "micronaut.http.services.foo.*") with a
45+
* name that matches the name used for security configuration (i.e. "micronaut.security.token.jwt.signatures.jwks.foo.*")
46+
* then that client will be used for the request. Otherwise, a default client will be used.
47+
*
48+
* @author Jeremy Grelle
49+
* @since 4.5.0
50+
*/
51+
@Singleton
52+
@Primary
53+
@Requires(classes = HttpClient.class)
54+
public class HttpClientJwksClient implements JwksClient {
55+
56+
private static final Logger LOG = LoggerFactory.getLogger(HttpClientJwksClient.class);
57+
58+
private final BeanContext beanContext;
59+
private final HttpClientRegistry<HttpClient> clientRegistry;
60+
private final Supplier<HttpClient> defaultJwkSetClient;
61+
private final ConcurrentHashMap<String, HttpClient> jwkSetClients = new ConcurrentHashMap<>();
62+
63+
public HttpClientJwksClient(BeanContext beanContext, HttpClientRegistry<HttpClient> clientRegistry, HttpClientConfiguration defaultClientConfiguration) {
64+
this.beanContext = beanContext;
65+
this.clientRegistry = clientRegistry;
66+
this.defaultJwkSetClient = SupplierUtil.memoized(() -> beanContext.createBean(HttpClient.class, LoadBalancer.empty(), defaultClientConfiguration));
67+
}
68+
69+
@Override
70+
public String load(@Nullable String providerName, @NonNull String url) throws HttpClientException {
71+
try {
72+
return Mono.from(getClient(providerName).retrieve(url)).block();
73+
} catch (HttpClientException e) {
74+
if (LOG.isErrorEnabled()) {
75+
LOG.error("Exception loading JWK from " + url, e);
76+
}
77+
}
78+
return null;
79+
}
80+
81+
/**
82+
* Retrieves an HTTP client for the given provider.
83+
*
84+
* @param providerName The provider name
85+
* @return An HTTP client to use to send the JWKS request
86+
*/
87+
protected HttpClient getClient(@Nullable String providerName) {
88+
if (providerName == null) {
89+
return defaultJwkSetClient.get();
90+
}
91+
return jwkSetClients.computeIfAbsent(providerName, provider ->
92+
beanContext.findBean(ServiceHttpClientConfiguration.class, Qualifiers.byName(provider))
93+
.map(serviceConfig -> this.clientRegistry.getClient(HttpVersionSelection.forClientConfiguration(serviceConfig), provider, "/"))
94+
.orElseGet(defaultJwkSetClient));
95+
}
96+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@ public interface JwkSetFetcher<T> {
3434
*
3535
* @param url The Jwks uri
3636
* @return The Json Web Key Set representation or an empty optional if it could not be loaded
37+
* @deprecated Use {@link #fetch(String, String)} instead.
3738
*/
3839
@NonNull
3940
@Blocking
41+
@Deprecated(since = "4.5.0", forRemoval = true)
4042
Optional<T> fetch(@Nullable String url);
4143

4244
/**
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2017-2023 original authors
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+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.micronaut.security.token.jwt.signature.jwks;
17+
18+
import io.micronaut.core.annotation.NonNull;
19+
import io.micronaut.core.annotation.Nullable;
20+
21+
/**
22+
* Client for loading Json Web Key Set content over http.
23+
*
24+
* @author Jeremy Grelle
25+
* @since 4.5.0
26+
*/
27+
public interface JwksClient {
28+
29+
/**
30+
* Loads remote Json Web Key Set content over http.
31+
*
32+
* @param providerName The jwks provider name
33+
* @param url The URL for loading the remote JWK Set
34+
* @return The JWK Set response body content
35+
*/
36+
@Nullable
37+
String load(@Nullable String providerName, @NonNull String url);
38+
}

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,14 +146,27 @@ public boolean verify(SignedJWT jwt) throws JOSEException {
146146
return JwksSignatureUtils.verify(jwt, computeJWKSet().orElse(null), jwkValidator);
147147
}
148148

149+
/**
150+
* Instantiates a JWKSet for a given url.
151+
* @param url JSON Web Key Set Url.
152+
* @return a JWKSet or null if there was an error.
153+
* @deprecated Use {@link #loadJwkSet(String, String)} instead.
154+
*/
155+
@Nullable
156+
@Deprecated(since = "4.5.0", forRemoval = true)
157+
protected JWKSet loadJwkSet(String url) {
158+
return jwkSetFetcher.fetch(null, url)
159+
.orElse(null);
160+
}
161+
149162
/**
150163
* Instantiates a JWKSet for a given url.
151164
* @param providerName The name of the JWKS configuration.
152165
* @param url JSON Web Key Set Url.
153166
* @return a JWKSet or null if there was an error.
154167
*/
155168
@Nullable
156-
protected JWKSet loadJwkSet(String providerName, String url) {
169+
protected JWKSet loadJwkSet(@Nullable String providerName, String url) {
157170
return jwkSetFetcher.fetch(providerName, url)
158171
.orElse(null);
159172
}

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,15 @@
1818
import com.nimbusds.jose.jwk.KeyType;
1919
import io.micronaut.core.annotation.NonNull;
2020
import io.micronaut.core.annotation.Nullable;
21+
import io.micronaut.core.naming.Named;
2122

2223
/**
2324
* JSON Web Key Set Configuration.
2425
*
2526
* @author Sergio del Amo
2627
* @since 1.1.0
2728
*/
28-
public interface JwksSignatureConfiguration {
29-
30-
@NonNull
31-
String getName();
29+
public interface JwksSignatureConfiguration extends Named {
3230

3331
/**
3432
* Json Web Key Set endpoint url.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public class JwksSignatureConfigurationProperties implements JwksSignatureConfig
4747
@SuppressWarnings("WeakerAccess")
4848
public static final int DEFAULT_CACHE_EXPIRATION = 60;
4949

50+
@Nullable
5051
private final String name;
5152

5253
@NonNull

0 commit comments

Comments
 (0)