From ec9ebddc279517f7e2e8d82aa377f9b8924dc510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20S=C3=A1nchez?= Date: Thu, 25 Jun 2026 10:27:40 +0200 Subject: [PATCH 1/2] feat: validate external-IdP tokens via remote JWKS + algorithm allowlist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an external-IdP mode to the resource server so apps fronting Keycloak/Cognito/etc. no longer hand-override the ReactiveJwtDecoder: - ResourceServerProperties: jwk-set-uri (remote JWKS endpoint) + signature-algorithms (RFC 8725 allowlist, default RS256/PS256/ES256). - fireflyReactiveJwtDecoder: when jwk-set-uri is set, build NimbusReactiveJwtDecoder.withJwkSetUri with the algorithm allowlist; otherwise keep the in-memory KeyManagementPort behaviour UNCHANGED. Validators (timestamp/issuer/audience) factored into buildValidators() and applied to both paths. @ConditionalOnMissingBean preserved (full override still possible). Backward compatible: consumers without jwk-set-uri get the exact previous behaviour — the existing ResourceServerIntegrationTest (in-memory path) stays green; a new unit test asserts the JWKS path is selected without touching the in-memory key. Dev version 26.06.03-SNAPSHOT so it coexists with the 26.06.02 release in .m2 without overwriting it; transitive framework security modules pinned to 26.06.02 (unchanged). --- pom.xml | 36 ++++++++++++++- .../rs/ResourceServerAutoConfiguration.java | 24 ++++++++-- .../security/rs/ResourceServerProperties.java | 15 ++++++ .../ResourceServerDecoderSelectionTest.java | 46 +++++++++++++++++++ 4 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 src/test/java/org/fireflyframework/security/rs/ResourceServerDecoderSelectionTest.java diff --git a/pom.xml b/pom.xml index a5cc968..d2fdc33 100644 --- a/pom.xml +++ b/pom.xml @@ -12,17 +12,49 @@ fireflyframework-security-resource-server - 26.06.02 + + 26.06.03-SNAPSHOT jar Firefly Framework - Security Resource Server Secure-by-default reactive OAuth2 resource server: JWT (and opaque) validation, claim-to-authority mapping, default-deny chain and hardened security headers. Delivered to applications via the application starter. + + + + + org.fireflyframework + fireflyframework-security-api + 26.06.02 + + + org.fireflyframework + fireflyframework-security-spi + 26.06.02 + + + org.fireflyframework + fireflyframework-security-core + 26.06.02 + + + org.fireflyframework + fireflyframework-security-webflux + 26.06.02 + + + + org.fireflyframework + fireflyframework-security-webflux - ${project.version} + 26.06.02 diff --git a/src/main/java/org/fireflyframework/security/rs/ResourceServerAutoConfiguration.java b/src/main/java/org/fireflyframework/security/rs/ResourceServerAutoConfiguration.java index 1c568e5..7f10dfd 100644 --- a/src/main/java/org/fireflyframework/security/rs/ResourceServerAutoConfiguration.java +++ b/src/main/java/org/fireflyframework/security/rs/ResourceServerAutoConfiguration.java @@ -122,15 +122,34 @@ public Converter> fireflyJwtAuthenticatio @Bean @ConditionalOnMissingBean public ReactiveJwtDecoder fireflyReactiveJwtDecoder(KeyManagementPort keyManagementPort, ResourceServerProperties properties) { + NimbusReactiveJwtDecoder decoder = buildDecoder(keyManagementPort, properties); + decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(buildValidators(properties))); + return decoder; + } + + /** + * Builds the decoder for an external IdP's remote JWKS when {@code jwk-set-uri} is configured + * (with the asymmetric algorithm allowlist), otherwise from the in-memory signing key of + * {@link KeyManagementPort} — the original, default behaviour, left unchanged. + */ + static NimbusReactiveJwtDecoder buildDecoder(KeyManagementPort keyManagementPort, ResourceServerProperties properties) { + if (properties.getJwkSetUri() != null && !properties.getJwkSetUri().isBlank()) { + return NimbusReactiveJwtDecoder.withJwkSetUri(properties.getJwkSetUri()) + .jwsAlgorithms(algorithms -> properties.getSignatureAlgorithms() + .forEach(name -> algorithms.add(SignatureAlgorithm.from(name)))) + .build(); + } SigningKey active = keyManagementPort.activeSigningKey().block(); if (active == null || !(active.publicKey() instanceof RSAPublicKey rsaPublicKey)) { throw new IllegalStateException("No RSA signing key available to build the JWT decoder"); } - NimbusReactiveJwtDecoder decoder = NimbusReactiveJwtDecoder + return NimbusReactiveJwtDecoder .withPublicKey(rsaPublicKey) .signatureAlgorithm(SignatureAlgorithm.RS256) .build(); + } + private static List> buildValidators(ResourceServerProperties properties) { List> validators = new ArrayList<>(); validators.add(new JwtTimestampValidator()); if (properties.getIssuer() != null && !properties.getIssuer().isBlank()) { @@ -139,8 +158,7 @@ public ReactiveJwtDecoder fireflyReactiveJwtDecoder(KeyManagementPort keyManagem if (!properties.getAudiences().isEmpty()) { validators.add(audienceValidator(properties.getAudiences())); } - decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(validators)); - return decoder; + return validators; } @Bean diff --git a/src/main/java/org/fireflyframework/security/rs/ResourceServerProperties.java b/src/main/java/org/fireflyframework/security/rs/ResourceServerProperties.java index 45c6bcd..82e3457 100644 --- a/src/main/java/org/fireflyframework/security/rs/ResourceServerProperties.java +++ b/src/main/java/org/fireflyframework/security/rs/ResourceServerProperties.java @@ -48,4 +48,19 @@ public class ResourceServerProperties { /** Claim paths inspected for OAuth2 scopes. */ private List scopeClaimPaths = new ArrayList<>(); + + /** + * Remote JWKS endpoint of an external IdP (e.g. Keycloak), e.g. + * {@code https://auth.example/realms/r/protocol/openid-connect/certs}. When set, the default + * decoder validates tokens against this JWKS — so a resource server fronting an external IdP needs + * no decoder override. When blank, the in-memory signing key ({@code KeyManagementPort}) is used, + * exactly as before (backward compatible). + */ + private String jwkSetUri; + + /** + * JWS signature algorithm allowlist applied to the external-JWKS decoder (RFC 8725): rejects + * {@code none} and symmetric algorithms. Asymmetric only; defaults to RS256/PS256/ES256. + */ + private List signatureAlgorithms = new ArrayList<>(List.of("RS256", "PS256", "ES256")); } diff --git a/src/test/java/org/fireflyframework/security/rs/ResourceServerDecoderSelectionTest.java b/src/test/java/org/fireflyframework/security/rs/ResourceServerDecoderSelectionTest.java new file mode 100644 index 0000000..87334ab --- /dev/null +++ b/src/test/java/org/fireflyframework/security/rs/ResourceServerDecoderSelectionTest.java @@ -0,0 +1,46 @@ +/* + * Copyright 2024-2026 Firefly Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.fireflyframework.security.rs; + +import org.fireflyframework.security.spi.KeyManagementPort; +import org.junit.jupiter.api.Test; +import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; + +/** + * Verifies decoder path selection. The in-memory (KeyManagementPort) path — the unchanged default — is + * covered end-to-end by {@link ResourceServerIntegrationTest}; this asserts that configuring + * {@code jwk-set-uri} switches to the external-JWKS decoder without touching the in-memory key. + */ +class ResourceServerDecoderSelectionTest { + + @Test + void usesRemoteJwksAndSkipsKeyManagementPortWhenJwkSetUriConfigured() { + KeyManagementPort keyManagementPort = mock(KeyManagementPort.class); + ResourceServerProperties properties = new ResourceServerProperties(); + properties.setJwkSetUri("https://idp.test/realms/r/protocol/openid-connect/certs"); + + NimbusReactiveJwtDecoder decoder = ResourceServerAutoConfiguration.buildDecoder(keyManagementPort, properties); + + assertThat(decoder).isNotNull(); + // The external-JWKS path must never consult the in-memory signing key. + verifyNoInteractions(keyManagementPort); + } +} From 714b8a72bdbc8524a7cdf73fb9dab8765b5398c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20S=C3=A1nchez?= Date: Fri, 26 Jun 2026 09:27:42 +0200 Subject: [PATCH 2/2] chore: normalize version to the 26.06.02 baseline ahead of the 26.06.03 framework release Drop the hand-rolled pre-release SNAPSHOT version pin and the dependencyManagement neutralization; the module returns to a clean 26.06.02 pom (the code changes stay). flywork fwversion bump will take the whole framework to 26.06.03 uniformly. --- pom.xml | 36 ++---------------------------------- 1 file changed, 2 insertions(+), 34 deletions(-) diff --git a/pom.xml b/pom.xml index d2fdc33..a5cc968 100644 --- a/pom.xml +++ b/pom.xml @@ -12,49 +12,17 @@ fireflyframework-security-resource-server - - 26.06.03-SNAPSHOT + 26.06.02 jar Firefly Framework - Security Resource Server Secure-by-default reactive OAuth2 resource server: JWT (and opaque) validation, claim-to-authority mapping, default-deny chain and hardened security headers. Delivered to applications via the application starter. - - - - - org.fireflyframework - fireflyframework-security-api - 26.06.02 - - - org.fireflyframework - fireflyframework-security-spi - 26.06.02 - - - org.fireflyframework - fireflyframework-security-core - 26.06.02 - - - org.fireflyframework - fireflyframework-security-webflux - 26.06.02 - - - - org.fireflyframework - fireflyframework-security-webflux - 26.06.02 + ${project.version}