Skip to content

Commit

Permalink
Offload JWK Set fetching from the event loop (#1641)
Browse files Browse the repository at this point in the history
JwtTokenValidator is modified to explicitly execute
JwtValidator.validate in a separate thread from the Netty event loop.

JwtValidator.validate is blocking in its current implementation and
this allows it to be executed in a non-blocking fashion.

This is a short-term fix for #1633, where the preferred long-term fix
would be to refactor the JwtValidator API to be non-blocking.
  • Loading branch information
jeremyg484 authored Mar 18, 2024
1 parent 0bdd444 commit afd2f69
Show file tree
Hide file tree
Showing 6 changed files with 412 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ public boolean verify(SignedJWT jwt) throws JOSEException {
@Blocking
@Deprecated(forRemoval = true, since = "4.5.0")
protected JWKSet loadJwkSet(String url) {
LOG.debug("Fetching JWK Set from {}", url);
return Mono.from(jwkSetFetcher.fetch(null, url))
.blockOptional()
.orElse(null);
Expand All @@ -173,6 +174,7 @@ protected JWKSet loadJwkSet(String url) {
@Nullable
@Blocking
protected JWKSet loadJwkSet(@Nullable String providerName, String url) {
LOG.debug("Fetching JWK Set from {}", url);
return Mono.from(jwkSetFetcher.fetch(providerName, url)).blockOptional().orElse(null);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,21 @@
package io.micronaut.security.token.jwt.validator;

import io.micronaut.core.annotation.Nullable;
import io.micronaut.scheduling.TaskExecutors;
import io.micronaut.security.authentication.Authentication;
import io.micronaut.security.token.jwt.encryption.EncryptionConfiguration;
import io.micronaut.security.token.jwt.signature.SignatureConfiguration;
import io.micronaut.security.token.validator.TokenValidator;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
import java.util.Collection;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;

import java.util.Collection;
import java.util.concurrent.ExecutorService;

/**
* @see <a href="https://connect2id.com/products/nimbus-jose-jwt/examples/validating-jwt-access-tokens">Validating JWT Access Tokens</a>
Expand All @@ -38,6 +44,7 @@ public class JwtTokenValidator<T> implements TokenValidator<T> {

protected final JwtAuthenticationFactory jwtAuthenticationFactory;
protected final JwtValidator<T> validator;
private final Scheduler scheduler;

/**
* Constructor.
Expand All @@ -46,8 +53,31 @@ public class JwtTokenValidator<T> implements TokenValidator<T> {
* @param encryptionConfigurations List of Encryption configurations which are used to attempt validation.
* @param genericJwtClaimsValidators Generic JWT Claims validators which should be used to validate any JWT.
* @param jwtAuthenticationFactory Utility to generate an Authentication given a JWT.
* @param executorService Executor Service
*/
@Inject
public JwtTokenValidator(Collection<SignatureConfiguration> signatureConfigurations,
Collection<EncryptionConfiguration> encryptionConfigurations,
Collection<GenericJwtClaimsValidator> genericJwtClaimsValidators,
JwtAuthenticationFactory jwtAuthenticationFactory,
@Named(TaskExecutors.BLOCKING) ExecutorService executorService) {
this(JwtValidator.builder()
.withSignatures(signatureConfigurations)
.withEncryptions(encryptionConfigurations)
.withClaimValidators(genericJwtClaimsValidators)
.build(), jwtAuthenticationFactory, Schedulers.fromExecutorService(executorService));
}

/**
* Constructor.
*
* @param signatureConfigurations List of Signature configurations which are used to attempt validation.
* @param encryptionConfigurations List of Encryption configurations which are used to attempt validation.
* @param genericJwtClaimsValidators Generic JWT Claims validators which should be used to validate any JWT.
* @param jwtAuthenticationFactory Utility to generate an Authentication given a JWT.
* @deprecated Use {@link #JwtTokenValidator(Collection, Collection, Collection, JwtAuthenticationFactory, ExecutorService)} instead.
*/
@Deprecated
public JwtTokenValidator(Collection<SignatureConfiguration> signatureConfigurations,
Collection<EncryptionConfiguration> encryptionConfigurations,
Collection<GenericJwtClaimsValidator> genericJwtClaimsValidators,
Expand All @@ -62,11 +92,25 @@ public JwtTokenValidator(Collection<SignatureConfiguration> signatureConfigurati
/**
* @param validator Validates the JWT
* @param jwtAuthenticationFactory The authentication factory
* @deprecated Use {@link #JwtTokenValidator(JwtValidator, JwtAuthenticationFactory, Scheduler)} instead.
*/
@Deprecated
public JwtTokenValidator(JwtValidator<T> validator,
JwtAuthenticationFactory jwtAuthenticationFactory) {
this(validator, jwtAuthenticationFactory, Schedulers.boundedElastic());
}

/**
* @param validator Validates the JWT
* @param jwtAuthenticationFactory The authentication factory
* @param scheduler The scheduler to use
*/
public JwtTokenValidator(JwtValidator<T> validator,
JwtAuthenticationFactory jwtAuthenticationFactory,
Scheduler scheduler) {
this.validator = validator;
this.jwtAuthenticationFactory = jwtAuthenticationFactory;
this.scheduler = scheduler;
}

/***
Expand All @@ -75,9 +119,8 @@ public JwtTokenValidator(JwtValidator<T> validator,
*/
@Override
public Publisher<Authentication> validateToken(String token, @Nullable T request) {
return validator.validate(token, request)
.flatMap(jwtAuthenticationFactory::createAuthentication)
.map(Flux::just)
.orElse(Flux.empty());
return Mono.fromCallable(() -> validator.validate(token, request))
.flatMap(tokenOptional -> tokenOptional.flatMap(jwtAuthenticationFactory::createAuthentication)
.map(Mono::just).orElse(Mono.empty())).subscribeOn(scheduler);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import io.micronaut.runtime.server.EmbeddedServer
import io.micronaut.security.testutils.YamlAsciidocTagCleaner
import io.micronaut.security.token.jwt.signature.jwks.HttpClientJwksClient
import io.micronaut.security.token.jwt.signature.jwks.JwksSignature
import io.micronaut.security.token.jwt.signature.jwks.ResourceRetrieverJwksClient
import org.yaml.snakeyaml.Yaml
import spock.lang.AutoCleanup
import spock.lang.Shared
Expand Down Expand Up @@ -43,6 +44,22 @@ micronaut:
awscognito:
url: '/eu-west-XXXX/.well-known/jwks.json'
#end::yamlserviceclientconfig[]
"""

String yamlServiceFallbackClientConfig = """
#tag::yamlservicefallbackclientconfig[]
micronaut:
security:
token:
jwt:
signatures:
jwks-client:
http-client:
enabled: false
jwks:
awscognito:
url: '/eu-west-XXXX/.well-known/jwks.json'
#end::yamlservicefallbackclientconfig[]
"""

@Shared
Expand Down Expand Up @@ -92,6 +109,30 @@ micronaut:
]
]

@Shared
Map<String, Object> serviceFallbackClientConfigMap = [
'micronaut': [
'security': [
'token': [
'jwt': [
'signatures': [
'jwks-client': [
'http-client': [
'enabled': false
]
],
'jwks': [
'awscognito': [
'url': '/eu-west-XXXX/.well-known/jwks.json'
]
]
]
]
]
]
]
]

@Shared
@AutoCleanup
EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, [
Expand All @@ -104,6 +145,12 @@ micronaut:
'spec.name': 'docjkwsSpec',
] << flatten(serviceClientConfigMap), Environment.TEST)

@Shared
@AutoCleanup
EmbeddedServer embeddedServiceFallbackClientServer = ApplicationContext.run(EmbeddedServer, [
'spec.name': 'docjkwsSpec',
] << flatten(serviceFallbackClientConfigMap), Environment.TEST)

void "JwksSignature bean exists in context"() {
expect:
new Yaml().load(cleanYamlAsciidocTag(yamlSecurityConfig)) == configMap
Expand All @@ -112,11 +159,20 @@ micronaut:
embeddedServer.applicationContext.containsBean(JwksSignature)
}

void "JwksClient bean exists in context"() {
void "HttpClientJwksClient bean exists in context"() {
expect:
new Yaml().load(cleanYamlAsciidocTag(yamlServiceClientConfig)) == serviceClientConfigMap

and:
embeddedServiceClientServer.applicationContext.containsBean(HttpClientJwksClient)
}

void "ResourceRetrieverJwksClient bean exists in context and HttpClientJwksClient is disabled"() {
expect:
new Yaml().load(cleanYamlAsciidocTag(yamlServiceFallbackClientConfig)) == serviceFallbackClientConfigMap

and:
embeddedServiceFallbackClientServer.applicationContext.containsBean(ResourceRetrieverJwksClient)
!embeddedServiceFallbackClientServer.applicationContext.containsBean(HttpClientJwksClient)
}
}
1 change: 1 addition & 0 deletions security-oauth2/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@ dependencies {
testImplementation(projects.testSuiteKeycloakDocker)
testImplementation(mnLogging.logback.classic)
testImplementation(libs.system.stubs.core)
testImplementation(mn.micronaut.retry)
}
Loading

0 comments on commit afd2f69

Please sign in to comment.