From afd2f692a7cb2cb215f40e4e827ea4de91227139 Mon Sep 17 00:00:00 2001 From: Jeremy Grelle Date: Mon, 18 Mar 2024 10:27:06 -0400 Subject: [PATCH] Offload JWK Set fetching from the event loop (#1641) 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. --- .../jwt/signature/jwks/JwksSignature.java | 2 + .../jwt/validator/JwtTokenValidator.java | 55 +++- .../io/micronaut/docs/jwks/JwksSpec.groovy | 58 +++- security-oauth2/build.gradle.kts | 1 + .../client/JwksUriSignatureTimeoutSpec.groovy | 296 ++++++++++++++++++ .../jwt/jwtValidation/jwks.adoc | 7 + 6 files changed, 412 insertions(+), 7 deletions(-) create mode 100644 security-oauth2/src/test/groovy/io/micronaut/security/oauth2/client/JwksUriSignatureTimeoutSpec.groovy diff --git a/security-jwt/src/main/java/io/micronaut/security/token/jwt/signature/jwks/JwksSignature.java b/security-jwt/src/main/java/io/micronaut/security/token/jwt/signature/jwks/JwksSignature.java index 7b1ef33421..81341c508f 100644 --- a/security-jwt/src/main/java/io/micronaut/security/token/jwt/signature/jwks/JwksSignature.java +++ b/security-jwt/src/main/java/io/micronaut/security/token/jwt/signature/jwks/JwksSignature.java @@ -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); @@ -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); } diff --git a/security-jwt/src/main/java/io/micronaut/security/token/jwt/validator/JwtTokenValidator.java b/security-jwt/src/main/java/io/micronaut/security/token/jwt/validator/JwtTokenValidator.java index e2b93b1bc9..ca5b07a02b 100644 --- a/security-jwt/src/main/java/io/micronaut/security/token/jwt/validator/JwtTokenValidator.java +++ b/security-jwt/src/main/java/io/micronaut/security/token/jwt/validator/JwtTokenValidator.java @@ -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 Validating JWT Access Tokens @@ -38,6 +44,7 @@ public class JwtTokenValidator implements TokenValidator { protected final JwtAuthenticationFactory jwtAuthenticationFactory; protected final JwtValidator validator; + private final Scheduler scheduler; /** * Constructor. @@ -46,8 +53,31 @@ public class JwtTokenValidator implements TokenValidator { * @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 signatureConfigurations, + Collection encryptionConfigurations, + Collection 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 signatureConfigurations, Collection encryptionConfigurations, Collection genericJwtClaimsValidators, @@ -62,11 +92,25 @@ public JwtTokenValidator(Collection signatureConfigurati /** * @param validator Validates the JWT * @param jwtAuthenticationFactory The authentication factory + * @deprecated Use {@link #JwtTokenValidator(JwtValidator, JwtAuthenticationFactory, Scheduler)} instead. */ + @Deprecated public JwtTokenValidator(JwtValidator 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 validator, + JwtAuthenticationFactory jwtAuthenticationFactory, + Scheduler scheduler) { this.validator = validator; this.jwtAuthenticationFactory = jwtAuthenticationFactory; + this.scheduler = scheduler; } /*** @@ -75,9 +119,8 @@ public JwtTokenValidator(JwtValidator validator, */ @Override public Publisher 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); } } diff --git a/security-jwt/src/test/groovy/io/micronaut/docs/jwks/JwksSpec.groovy b/security-jwt/src/test/groovy/io/micronaut/docs/jwks/JwksSpec.groovy index ef38d221fa..c153a73071 100644 --- a/security-jwt/src/test/groovy/io/micronaut/docs/jwks/JwksSpec.groovy +++ b/security-jwt/src/test/groovy/io/micronaut/docs/jwks/JwksSpec.groovy @@ -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 @@ -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 @@ -92,6 +109,30 @@ micronaut: ] ] + @Shared + Map 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, [ @@ -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 @@ -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) + } } diff --git a/security-oauth2/build.gradle.kts b/security-oauth2/build.gradle.kts index 10fa19d23b..5195bda849 100644 --- a/security-oauth2/build.gradle.kts +++ b/security-oauth2/build.gradle.kts @@ -29,4 +29,5 @@ dependencies { testImplementation(projects.testSuiteKeycloakDocker) testImplementation(mnLogging.logback.classic) testImplementation(libs.system.stubs.core) + testImplementation(mn.micronaut.retry) } diff --git a/security-oauth2/src/test/groovy/io/micronaut/security/oauth2/client/JwksUriSignatureTimeoutSpec.groovy b/security-oauth2/src/test/groovy/io/micronaut/security/oauth2/client/JwksUriSignatureTimeoutSpec.groovy new file mode 100644 index 0000000000..730883408f --- /dev/null +++ b/security-oauth2/src/test/groovy/io/micronaut/security/oauth2/client/JwksUriSignatureTimeoutSpec.groovy @@ -0,0 +1,296 @@ +package io.micronaut.security.oauth2.client + +import com.nimbusds.jose.JOSEException +import com.nimbusds.jose.JWSAlgorithm +import com.nimbusds.jose.jwk.JWK +import com.nimbusds.jose.jwk.KeyUse +import com.nimbusds.jose.jwk.RSAKey +import com.nimbusds.jose.jwk.gen.RSAKeyGenerator +import com.nimbusds.jwt.JWTClaimsSet +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Property +import io.micronaut.context.annotation.Replaces +import io.micronaut.context.annotation.Requires +import io.micronaut.context.annotation.Value +import io.micronaut.core.annotation.Nullable +import io.micronaut.core.io.socket.SocketUtils +import io.micronaut.http.* +import io.micronaut.http.annotation.Consumes +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Header +import io.micronaut.http.annotation.Produces +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.retry.annotation.Retryable +import io.micronaut.runtime.ApplicationConfiguration +import io.micronaut.runtime.server.EmbeddedServer +import io.micronaut.security.annotation.Secured +import io.micronaut.security.authentication.UsernamePasswordCredentials +import io.micronaut.security.rules.SecurityRule +import io.micronaut.security.testutils.authprovider.MockAuthenticationProvider +import io.micronaut.security.testutils.authprovider.SuccessAuthenticationScenario +import io.micronaut.security.token.claims.ClaimsAudienceProvider +import io.micronaut.security.token.claims.JtiGenerator +import io.micronaut.security.token.config.TokenConfiguration +import io.micronaut.security.token.jwt.endpoints.JwkProvider +import io.micronaut.security.token.jwt.generator.claims.JWTClaimsSetGenerator +import io.micronaut.security.token.jwt.signature.rsa.RSASignatureGeneratorConfiguration +import io.micronaut.security.token.render.BearerAccessRefreshToken +import jakarta.inject.Named +import jakarta.inject.Singleton +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import spock.lang.Specification + +import java.security.Principal +import java.security.interfaces.RSAPrivateKey +import java.security.interfaces.RSAPublicKey +import java.util.concurrent.atomic.AtomicInteger + +class JwksUriSignatureTimeoutSpec extends Specification { + + static final String SPEC_NAME_PROPERTY = 'spec.name' + + def "authorization does not fail when loading JWKS with limited thread resources"() { + given: + EmbeddedServer authEmbeddedServer = ApplicationContext.run(EmbeddedServer, [ + (SPEC_NAME_PROPERTY) : 'JwksUriSignatureTimeoutSpec.auth', + 'retry-jwks' : false, + 'micronaut.server.port': SocketUtils.findAvailableTcpPort(), + 'micronaut.security.authentication' : 'bearer' + ]) + EmbeddedServer echoEmbeddedServer = ApplicationContext.run(EmbeddedServer, [ + (SPEC_NAME_PROPERTY) : 'JwksUriSignatureTimeoutSpec.books', + 'micronaut.netty.event-loops.default.num-threads': 1, + 'micronaut.http.client.read-timeout': '1s', + 'micronaut.security.authentication': 'idtoken', + 'micronaut.security.oauth2.clients.a.client-id': "XXX", + 'micronaut.security.oauth2.clients.a.openid.issuer' : "http://localhost:${authEmbeddedServer.port}/oauth2/default" + ]) + EmbeddedServer clientServer = ApplicationContext.run(EmbeddedServer, [ + (SPEC_NAME_PROPERTY) : 'JwksUriSignatureTimeoutSpec.client', + 'books-server.url' : echoEmbeddedServer.getURL() + ]) + + HttpClient authClient = authEmbeddedServer.applicationContext.createBean(HttpClient, authEmbeddedServer.getURL()) + EchoClient echoClient = clientServer.applicationContext.createBean(EchoClient) + + when: + UsernamePasswordCredentials creds = new UsernamePasswordCredentials('user', 'password') + HttpResponse rsp = authClient.toBlocking().exchange(HttpRequest.POST('/login', creds), BearerAccessRefreshToken) + + then: + rsp.status() == HttpStatus.OK + rsp.body().accessToken + !rsp.body().refreshToken + + when: + String result = echoClient.getUserName(HttpHeaderValues.AUTHORIZATION_PREFIX_BEARER + " " + rsp.body().accessToken) + + then: + result == "user" + + cleanup: + authEmbeddedServer.close() + echoEmbeddedServer.close() + clientServer.close() + } + + def "authorization can use configured retry when loading JWKS with limited thread resources"() { + given: + EmbeddedServer authEmbeddedServer = ApplicationContext.run(EmbeddedServer, [ + (SPEC_NAME_PROPERTY) : 'JwksUriSignatureTimeoutSpec.auth', + 'retry-jwks' : true, + 'micronaut.server.port': SocketUtils.findAvailableTcpPort(), + 'micronaut.security.authentication' : 'bearer' + ]) + EmbeddedServer echoEmbeddedServer = ApplicationContext.run(EmbeddedServer, [ + (SPEC_NAME_PROPERTY) : 'JwksUriSignatureTimeoutSpec.books', + 'micronaut.netty.event-loops.default.num-threads': 1, + 'micronaut.http.client.read-timeout': '1s', + 'micronaut.security.authentication': 'idtoken', + 'micronaut.security.oauth2.clients.a.client-id': "XXX", + 'micronaut.security.oauth2.clients.a.openid.issuer' : "http://localhost:${authEmbeddedServer.port}/oauth2/default" + ]) + EmbeddedServer clientServer = ApplicationContext.run(EmbeddedServer, [ + (SPEC_NAME_PROPERTY) : 'JwksUriSignatureTimeoutSpec.client-retry', + 'books-server.url' : echoEmbeddedServer.getURL() + ]) + + HttpClient authClient = authEmbeddedServer.applicationContext.createBean(HttpClient, authEmbeddedServer.getURL()) + RetryableEchoClient echoClient = clientServer.applicationContext.createBean(RetryableEchoClient) + + when: + UsernamePasswordCredentials creds = new UsernamePasswordCredentials('user', 'password') + HttpResponse rsp = authClient.toBlocking().exchange(HttpRequest.POST('/login', creds), BearerAccessRefreshToken) + + then: + rsp.status() == HttpStatus.OK + rsp.body().accessToken + !rsp.body().refreshToken + + when: + String result = echoClient.getUserName(HttpHeaderValues.AUTHORIZATION_PREFIX_BEARER + " " + rsp.body().accessToken) + + then: + result == "user" + + cleanup: + authEmbeddedServer.close() + echoEmbeddedServer.close() + clientServer.close() + } + + @Requires(property = 'spec.name', value = 'JwksUriSignatureTimeoutSpec.client') + @Client('${books-server.url}') + static interface EchoClient { + @Consumes(MediaType.TEXT_PLAIN) + @Get("/user") + String getUserName(@Header(HttpHeaders.AUTHORIZATION) String bearerToken) + } + + @Requires(property = 'spec.name', value = 'JwksUriSignatureTimeoutSpec.client-retry') + @Client('${books-server.url}') + @Retryable(attempts = '2') + static interface RetryableEchoClient { + @Consumes(MediaType.TEXT_PLAIN) + @Get("/user") + String getUserName(@Header(HttpHeaders.AUTHORIZATION) String bearerToken) + } + + @Requires(property = 'spec.name', value = 'JwksUriSignatureTimeoutSpec.books') + @Controller("/user") + @Secured(SecurityRule.IS_AUTHENTICATED) + static class EchoController { + + @Produces(MediaType.TEXT_PLAIN) + @Get + String username(Principal principal) { + principal.name + } + } + + @Singleton + @Requires(property = 'spec.name', value = 'JwksUriSignatureTimeoutSpec.auth') + static class AuthenticationProviderUserPassword extends MockAuthenticationProvider { + AuthenticationProviderUserPassword() { + super([new SuccessAuthenticationScenario( 'user')]) + } + } + + @Requires(property = 'spec.name', value = 'JwksUriSignatureTimeoutSpec.auth') + @Replaces(JWTClaimsSetGenerator) + @Singleton + static class AuthServerACustomJWTClaimsSetGenerator extends JWTClaimsSetGenerator { + Integer port + AuthServerACustomJWTClaimsSetGenerator(TokenConfiguration tokenConfiguration, + @Nullable JtiGenerator jwtIdGenerator, + @Nullable ClaimsAudienceProvider claimsAudienceProvider, + @Nullable ApplicationConfiguration applicationConfiguration, + @Value('${micronaut.server.port}') Integer port) { + super(tokenConfiguration, jwtIdGenerator, claimsAudienceProvider, applicationConfiguration) + this.port = port + } + + @Override + protected void populateIss(JWTClaimsSet.Builder builder) { + builder.issuer("http://localhost:${port}/oauth2/default") + } + + @Override + protected void populateAud(JWTClaimsSet.Builder builder) { + builder.audience("XXX") + } + } + + @Requires(property = 'spec.name', value = 'JwksUriSignatureTimeoutSpec.auth') + @Controller("/oauth2/default/.well-known/openid-configuration") + static class AuthServerOpenIdConfigurationController { + Integer port + AuthServerOpenIdConfigurationController(@Value('${micronaut.server.port}') Integer port) { + this.port = port + } + @Secured(SecurityRule.IS_ANONYMOUS) + @Get + String index() { + '{"issuer":"http://localhost:' + port + '/oauth2/default","authorization_endpoint":"https://dev-133320.okta.com/oauth2/default/v1/authorize","token_endpoint":"https://dev-133320.okta.com/oauth2/default/v1/token","userinfo_endpoint":"https://dev-133320.okta.com/oauth2/default/v1/userinfo","registration_endpoint":"https://dev-133320.okta.com/oauth2/v1/clients","jwks_uri":"http://localhost:' + port + '/keys","response_types_supported":["code","id_token","code id_token","code token","id_token token","code id_token token"],"response_modes_supported":["query","fragment","form_post","okta_post_message"],"grant_types_supported":["authorization_code","implicit","refresh_token","password"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["RS256"],"scopes_supported":["openid","profile","email","address","phone","offline_access"],"token_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post","client_secret_jwt","private_key_jwt","none"],"claims_supported":["iss","ver","sub","aud","iat","exp","jti","auth_time","amr","idp","nonce","name","nickname","preferred_username","given_name","middle_name","family_name","email","email_verified","profile","zoneinfo","locale","address","phone_number","picture","website","gender","birthdate","updated_at","at_hash","c_hash"],"code_challenge_methods_supported":["S256"],"introspection_endpoint":"https://dev-133320.okta.com/oauth2/default/v1/introspect","introspection_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post","client_secret_jwt","private_key_jwt","none"],"revocation_endpoint":"https://dev-133320.okta.com/oauth2/default/v1/revoke","revocation_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post","client_secret_jwt","private_key_jwt","none"],"end_session_endpoint":"https://dev-133320.okta.com/oauth2/default/v1/logout","request_parameter_supported":true,"request_object_signing_alg_values_supported":["HS256","HS384","HS512","RS256","RS384","RS512","ES256","ES384","ES512"]}' + } + } + + @Named("generator") + @Singleton + @Requires(property = 'spec.name', value = 'JwksUriSignatureTimeoutSpec.auth') + static class SlowJwkProvider implements JwkProvider, RSASignatureGeneratorConfiguration { + + private RSAKey jwk + + private boolean initialized = false + + //@Inject + @Property(name = 'retry-jwks') + boolean isRetry + + private AtomicInteger keyRequestCount = new AtomicInteger(0) + + private static final Logger LOG = LoggerFactory.getLogger(SlowJwkProvider.class) + + SlowJwkProvider() { + String keyId = UUID.randomUUID().toString() + try { + this.jwk = new RSAKeyGenerator(2048) + .algorithm(JWSAlgorithm.RS256) + .keyUse(KeyUse.SIGNATURE) // indicate the intended use of the key + .keyID(keyId) // give the key a unique ID + .generate() + + } catch (JOSEException e) { + + } + } + + @Override + List retrieveJsonWebKeys() { + if (initialized && isRetry) { + if (keyRequestCount.incrementAndGet() < 3) { + Thread.sleep(6000) + } + } else { + initialized = true + } + [jwk] + } + + @Override + RSAPrivateKey getPrivateKey() { + try { + return jwk.toRSAPrivateKey() + } catch (JOSEException e) { + if (LOG.isErrorEnabled()) { + LOG.error("JOSEException getting RSA private key", e) + } + } + return null + } + + @Override + JWSAlgorithm getJwsAlgorithm() { + if (jwk.getAlgorithm() instanceof JWSAlgorithm) { + return (JWSAlgorithm) jwk.getAlgorithm() + } + return null + } + + @Override + RSAPublicKey getPublicKey() { + try { + return jwk.toRSAPublicKey() + } catch (JOSEException e) { + if (LOG.isErrorEnabled()) { + LOG.error("JOSEException getting RSA public key", e) + } + } + return null + } + } +} diff --git a/src/main/docs/guide/authenticationStrategies/jwt/jwtValidation/jwks.adoc b/src/main/docs/guide/authenticationStrategies/jwt/jwtValidation/jwks.adoc index 50a3a52c07..02e72312dd 100644 --- a/src/main/docs/guide/authenticationStrategies/jwt/jwtValidation/jwks.adoc +++ b/src/main/docs/guide/authenticationStrategies/jwt/jwtValidation/jwks.adoc @@ -20,5 +20,12 @@ include::{testssecurityjwt}/jwks/JwksSpec.groovy[indent=0, tag=yamlserviceclient Note that the same approach will be applied when using a `jwks_uri` supplied via <> metadata. +The https://docs.micronaut.io/latest/guide/#httpClient[Micronaut HTTP Client] based implementation can be explicitly disabled in favor of the JWT library's internal resource fetching implementation by explicitly setting `micronaut.security.token.jwt.signatures.jwks-client.http-client.enabled=false`. For example: + +[configuration] +---- +include::{testssecurityjwt}/jwks/JwksSpec.groovy[indent=0, tag=yamlservicefallbackclientconfig] +---- + If you want to expose your own JWK Set, read the <> section.