The ConfigurableJWTProcessor.setJWSKeySelector(JWSKeySelector) is unable to update the reference used by NimbusReactiveJwtDecoder.JwkSetUriReactiveJwtDecoderBuilder.processor(). The processor holds a reference to the default JWSKeySelector created within NimbusReactiveJwtDecoder.JwkSetUriReactiveJwtDecoderBuilder.jwsKeySelector() via a method reference within JwkSetUriReactiveJwtDecoderBuilder.getExpectedJwsAlgorithms() before the ConfigurableJWTProcessorsetJWSKeySelector(JWSKeySelector) is executed.

The following unit test will show that the processor is executing the wrong JWSVerificationKeySelector.isAllowed(JWSAlgorithm) reference.

@ExtendWith(MockitoExtension.class)
class NimbusReactiveJwtDecoderTest {

    @Mock private WebClient webClient;
    @Mock private RequestHeadersUriSpec<?> requestHeadersUriSpec;
    @Mock private ResponseSpec responseSpec;

    @Test
    void customizeJWSKeySelector() throws Exception {

        var jwkSet = new JWKSet(
                new RSAKeyGenerator(RSAKeyGenerator.MIN_KEY_SIZE_BITS)
                .algorithm(JWSAlgorithm.RS512)
                .keyID(UUID.randomUUID().toString())
                .keyUse(KeyUse.SIGNATURE)
                .generate());

        var jwkSource = new ImmutableJWKSet<>(jwkSet);

        var jwt = new NimbusJwtEncoder(jwkSource).encode(
                JwtEncoderParameters.from(JwsHeader.with(SignatureAlgorithm.RS512).build(), 
                        JwtClaimsSet.builder().issuer("issuer").build()));

        // stub WebClient response for ReactiveRemoteJWKSource
        Mockito.doReturn(requestHeadersUriSpec).when(webClient).get();
        Mockito.doReturn(requestHeadersUriSpec).when(requestHeadersUriSpec).uri(Mockito.anyString());
        Mockito.when(requestHeadersUriSpec.retrieve()).thenReturn(responseSpec);
        Mockito.when(responseSpec.bodyToMono(String.class)).thenReturn(Mono.just(jwkSet.toString()));

        var decoder = NimbusReactiveJwtDecoder
                .withJwkSetUri("http://localhost/oauth2/jwks") // use NimbusReactiveJwtDecoder.JwkSetUriReactiveJwtDecoderBuilder
                .webClient(webClient) // stub remote call
                .jwtProcessorCustomizer(processor -> {

                    // this reference will not be used by the "Function<JWSAlgorithm, Boolean> expectedJwsAlgorithms" within JwkSetUriReactiveJwtDecoderBuilder.processor()
                    var jwsKeySelector = new JWSVerificationKeySelector<>(Set.of(JWSAlgorithm.RS512),  jwkSource);

                    // the JwkSetUriReactiveJwtDecoderBuilder.processor() still holds a reference to the JWSVerificationKeySelector instantiated
                    // within JwkSetUriReactiveJwtDecoderBuilder.jwsKeySelector(), via the method reference created by JwkSetUriReactiveJwtDecoderBuilder.getExpectedJwsAlgorithms(), held before the customizer is executed.
                    processor.setJWSKeySelector((JWSVerificationKeySelector) jwsKeySelector);
                })
                .build();

        // execute the processor
        decoder.decode(jwt.getTokenValue())
            .block();
    }

}

Comment From: jzheaux

Thanks, @meverden, for the observation. Are you able to submit a PR that addresses the issue? It would be nice to include your test as part of the contribution.

Comment From: meverden

@jzheaux, yes, I'll submit a PR.

Comment From: meverden

@jzheaux , https://github.com/spring-projects/spring-security/pull/12965 has been submitted.