This is an automated email from the ASF dual-hosted git repository.

nscendoni pushed a commit to branch master
in repository 
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-auth-oauth-client.git


The following commit(s) were added to refs/heads/master by this push:
     new 248ea57  SLING-13315: Make OIDC JWK set retrieval HTTP limits 
configurable (#51)
248ea57 is described below

commit 248ea579e9851deefd9219d0d57d83965d70656c
Author: Nicola Scendoni <[email protected]>
AuthorDate: Thu Aug 27 14:01:10 2026 +0200

    SLING-13315: Make OIDC JWK set retrieval HTTP limits configurable (#51)
    
    * SLING-13315: Make OIDC JWK set retrieval HTTP limits configurable
    
    The OidcAuthenticationHandler built the IDTokenValidator with the
    constructor that uses Nimbus' default DefaultResourceRetriever, whose
    HTTP entity size limit is fixed at 50 KB. IdPs that publish a large JWK
    set cause token validation to fail with:
    
      Couldn't retrieve remote JWK set: Exceeded configured input limit of
      51200 bytes
    
    Add three OSGi config attributes (jwkSetHttpSizeLimit,
    jwkSetHttpConnectTimeout, jwkSetHttpReadTimeout) and pass an explicitly
    configured DefaultResourceRetriever to IDTokenValidator. Defaults match
    the previous Nimbus defaults (51200 bytes, 500 ms, 500 ms) so behaviour
    is unchanged unless overridden.
    
    Co-Authored-By: Claude Opus 4.8 <[email protected]>
    
    * SLING-13315: Fix SonarCloud S5778 in JWK size limit test
    
    Hoist createIdToken() and createMockCookies() out of the assertThrows
    lambda so it contains a single invocation that may throw a runtime
    exception.
    
    Co-Authored-By: Claude Opus 4.8 <[email protected]>
    
    * SLING-13315: Use actual IdP server URL in JWK size limit tests
    
    Replace the hardcoded http://localhost:4567 base URL with the real
    idpServer address so the tests do not rely on a magic port value.
    
    Co-Authored-By: Claude Opus 4.8 <[email protected]>
    
    * SLING-13315: Validate JWK set retrieval limits and forbid unbounded values
    
    Address review findings on the new JWK set HTTP settings:
    
    - Reject unbounded values: Nimbus treats 0 as infinite for the size
      limit and timeouts, which removes DoS protection on the authentication
      path. Require positive values and cap them at defensible upper bounds
      (10 MB size, 60 s timeouts).
    - Fail fast on bad configuration: values are now validated at activation
      (requireInRange) so a misconfiguration throws immediately with a clear,
      property-named message instead of activating successfully and failing
      on every later OIDC callback. Added metatype min/max constraints.
    
    Adds unit tests for zero, negative and above-max configurations.
    
    Co-Authored-By: Claude Opus 4.8 <[email protected]>
    
    ---------
    
    Co-authored-by: Claude Opus 4.8 <[email protected]>
---
 .../impl/OidcAuthenticationHandler.java            | 83 +++++++++++++++++++++-
 .../impl/OidcAuthenticationHandlerTest.java        | 65 +++++++++++++++++
 2 files changed, 146 insertions(+), 2 deletions(-)

diff --git 
a/src/main/java/org/apache/sling/auth/oauth_client/impl/OidcAuthenticationHandler.java
 
b/src/main/java/org/apache/sling/auth/oauth_client/impl/OidcAuthenticationHandler.java
index 91ac2b7..4ea79dd 100644
--- 
a/src/main/java/org/apache/sling/auth/oauth_client/impl/OidcAuthenticationHandler.java
+++ 
b/src/main/java/org/apache/sling/auth/oauth_client/impl/OidcAuthenticationHandler.java
@@ -37,6 +37,7 @@ import java.util.stream.Stream;
 import com.nimbusds.jose.JOSEException;
 import com.nimbusds.jose.JWSAlgorithm;
 import com.nimbusds.jose.proc.BadJOSEException;
+import com.nimbusds.jose.util.DefaultResourceRetriever;
 import com.nimbusds.oauth2.sdk.AuthorizationCode;
 import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
 import com.nimbusds.oauth2.sdk.AuthorizationResponse;
@@ -93,6 +94,17 @@ public class OidcAuthenticationHandler extends 
DefaultAuthenticationFeedbackHand
     public static final String REDIRECT_ATTRIBUTE_NAME = "sling.redirect";
 
     private static final Logger logger = 
LoggerFactory.getLogger(OidcAuthenticationHandler.class);
+
+    // Nimbus RemoteJWKSet defaults, kept in sync so the default behaviour is 
unchanged.
+    static final int DEFAULT_JWK_SET_HTTP_SIZE_LIMIT = 50 * 1024;
+    static final int DEFAULT_JWK_SET_HTTP_CONNECT_TIMEOUT = 500;
+    static final int DEFAULT_JWK_SET_HTTP_READ_TIMEOUT = 500;
+
+    // Defensible bounds for the JWK set retrieval settings. These guard the 
authentication path against a
+    // compromised or malfunctioning IdP: an unbounded ("0") size/timeout is 
not allowed (Nimbus treats 0 as
+    // infinite), and the values are capped so no single value can be 
misconfigured to an abusive amount.
+    static final int MAX_JWK_SET_HTTP_SIZE_LIMIT = 10 * 1024 * 1024; // 10 MB
+    static final int MAX_JWK_SET_HTTP_TIMEOUT = 60_000; // 60 s
     private static final String AUTH_TYPE = "oidc";
 
     private final Map<String, ClientConnection> connections;
@@ -127,6 +139,12 @@ public class OidcAuthenticationHandler extends 
DefaultAuthenticationFeedbackHand
 
     private final OidcLogoutHandler logoutHandler;
 
+    private final int jwkSetHttpSizeLimit;
+
+    private final int jwkSetHttpConnectTimeout;
+
+    private final int jwkSetHttpReadTimeout;
+
     @ObjectClassDefinition(
             name = "Apache Sling Oidc Authentication Handler",
             description = "Apache Sling Oidc Authentication Handler Service")
@@ -196,6 +214,35 @@ public class OidcAuthenticationHandler extends 
DefaultAuthenticationFeedbackHand
                         + "logout is enabled and this is empty, the service 
will fail to activate with an exception.",
                 cardinality = Integer.MAX_VALUE)
         String[] logoutRedirectAllowedHosts() default {};
+
+        @AttributeDefinition(
+                name = "JWK set HTTP size limit (bytes)",
+                description = "Maximum size, in bytes, of the JWK set document 
fetched from the IdP's jwks_uri "
+                        + "when validating the ID token signature. Increase 
this if the IdP publishes a large key "
+                        + "set and token validation fails with 'Exceeded 
configured input limit'. Must be between "
+                        + "1 and 10485760 (10 MB); an unbounded value is not 
allowed on the authentication path. "
+                        + "Default is 51200 (50 KB), the Nimbus library 
default.",
+                min = "1",
+                max = "" + MAX_JWK_SET_HTTP_SIZE_LIMIT)
+        int jwkSetHttpSizeLimit() default DEFAULT_JWK_SET_HTTP_SIZE_LIMIT;
+
+        @AttributeDefinition(
+                name = "JWK set HTTP connect timeout (ms)",
+                description = "Connect timeout, in milliseconds, for 
retrieving the JWK set from the IdP's "
+                        + "jwks_uri. Must be between 1 and 60000; an unbounded 
value is not allowed on the "
+                        + "authentication path. Default is 500.",
+                min = "1",
+                max = "" + MAX_JWK_SET_HTTP_TIMEOUT)
+        int jwkSetHttpConnectTimeout() default 
DEFAULT_JWK_SET_HTTP_CONNECT_TIMEOUT;
+
+        @AttributeDefinition(
+                name = "JWK set HTTP read timeout (ms)",
+                description = "Read timeout, in milliseconds, for retrieving 
the JWK set from the IdP's "
+                        + "jwks_uri. Must be between 1 and 60000; an unbounded 
value is not allowed on the "
+                        + "authentication path. Default is 500.",
+                min = "1",
+                max = "" + MAX_JWK_SET_HTTP_TIMEOUT)
+        int jwkSetHttpReadTimeout() default DEFAULT_JWK_SET_HTTP_READ_TIMEOUT;
     }
 
     @Activate
@@ -230,6 +277,14 @@ public class OidcAuthenticationHandler extends 
DefaultAuthenticationFeedbackHand
                 : Set.of();
         this.cryptoService = cryptoService;
         this.logoutHandler = logoutHandler;
+        // Validate the JWK set retrieval settings at activation so a bad 
configuration fails immediately
+        // with a clear message, rather than silently activating and failing 
on every later OIDC callback.
+        this.jwkSetHttpSizeLimit =
+                requireInRange(config.jwkSetHttpSizeLimit(), 
"jwkSetHttpSizeLimit", MAX_JWK_SET_HTTP_SIZE_LIMIT);
+        this.jwkSetHttpConnectTimeout =
+                requireInRange(config.jwkSetHttpConnectTimeout(), 
"jwkSetHttpConnectTimeout", MAX_JWK_SET_HTTP_TIMEOUT);
+        this.jwkSetHttpReadTimeout =
+                requireInRange(config.jwkSetHttpReadTimeout(), 
"jwkSetHttpReadTimeout", MAX_JWK_SET_HTTP_TIMEOUT);
 
         // Security validation: enforce allowed hosts when SP-initiated single 
logout is enabled
         if (this.enableSPInitiatedSingleLogout && 
this.logoutRedirectAllowedHosts.isEmpty()) {
@@ -249,6 +304,27 @@ public class OidcAuthenticationHandler extends 
DefaultAuthenticationFeedbackHand
         logger.info("OidcAuthenticationHandler successfully activated");
     }
 
+    /**
+     * Validates that a JWK set retrieval setting is a positive value within 
the allowed range. A value of 0
+     * (which Nimbus interprets as "unlimited"/"no timeout") or any negative 
value is rejected, as an unbounded
+     * network fetch on the authentication path is a denial-of-service risk.
+     *
+     * @param value the configured value
+     * @param name  the configuration property name, used in the error message
+     * @param max   the inclusive upper bound
+     * @return the validated value
+     * @throws IllegalArgumentException if the value is outside {@code [1, 
max]}
+     */
+    private static int requireInRange(int value, @NotNull String name, int 
max) {
+        if (value < 1 || value > max) {
+            throw new IllegalArgumentException(String.format(
+                    "SECURITY: '%s' must be between 1 and %d but was %d. An 
unbounded (0 or negative) value is "
+                            + "not allowed for JWK set retrieval on the 
authentication path.",
+                    name, max, value));
+        }
+        return value;
+    }
+
     @Override
     public AuthenticationInfo extractCredentials(
             @NotNull HttpServletRequest request, @NotNull HttpServletResponse 
response) {
@@ -511,7 +587,7 @@ public class OidcAuthenticationHandler extends 
DefaultAuthenticationFeedbackHand
      * @param conn         The resolved OIDC connection.
      * @return The validated ID token claims set.
      */
-    private static @NotNull IDTokenClaimsSet validateIdToken(
+    private @NotNull IDTokenClaimsSet validateIdToken(
             @NotNull TokenResponse tokenResponse, @NotNull 
ResolvedOidcConnection conn, Nonce nonce) {
         Issuer issuer = new Issuer(conn.issuer());
         ClientID clientID = new ClientID(conn.clientId());
@@ -519,7 +595,10 @@ public class OidcAuthenticationHandler extends 
DefaultAuthenticationFeedbackHand
             JWSAlgorithm jwsAlg = JWSAlgorithm.RS256; // TODO: Read from config
             URL jwkSetURL = conn.jwkSetURL().toURL();
 
-            IDTokenValidator validator = new IDTokenValidator(issuer, 
clientID, jwsAlg, jwkSetURL);
+            // Use a resource retriever with configurable HTTP limits so large 
JWK sets can be fetched.
+            DefaultResourceRetriever resourceRetriever =
+                    new DefaultResourceRetriever(jwkSetHttpConnectTimeout, 
jwkSetHttpReadTimeout, jwkSetHttpSizeLimit);
+            IDTokenValidator validator = new IDTokenValidator(issuer, 
clientID, jwsAlg, jwkSetURL, resourceRetriever);
             return validator.validate(
                     
tokenResponse.toSuccessResponse().getTokens().toOIDCTokens().getIDToken(), 
nonce);
         } catch (BadJOSEException | JOSEException | MalformedURLException e) {
diff --git 
a/src/test/java/org/apache/sling/auth/oauth_client/impl/OidcAuthenticationHandlerTest.java
 
b/src/test/java/org/apache/sling/auth/oauth_client/impl/OidcAuthenticationHandlerTest.java
index ea99ac0..f07d87d 100644
--- 
a/src/test/java/org/apache/sling/auth/oauth_client/impl/OidcAuthenticationHandlerTest.java
+++ 
b/src/test/java/org/apache/sling/auth/oauth_client/impl/OidcAuthenticationHandlerTest.java
@@ -463,6 +463,71 @@ class OidcAuthenticationHandlerTest {
                 "testUser", ((OidcAuthCredentials) 
authInfo.get("user.jcr.credentials")).getAttribute("profile/name"));
     }
 
+    @Test
+    void 
extractCredentials_WithJwkSetHttpSizeLimitTooSmall_FailsToRetrieveJwkSet() 
throws JOSEException {
+        // A very small JWK set size limit must cause the JWK set retrieval to 
fail during token validation.
+        config = createConfig(Map.of("userInfoEnabled", true, 
"jwkSetHttpSizeLimit", 10));
+
+        RSAKey rsaJWK = new RSAKeyGenerator(2048).keyID("123").generate();
+        String idToken = createIdToken(rsaJWK, "client-id", ISSUER);
+        Cookie[] cookies = createMockCookies();
+        String baseUrl = "http://localhost:"; + 
idpServer.getAddress().getPort();
+        RuntimeException exception = assertThrows(
+                RuntimeException.class,
+                () -> 
extractCredentials_WithMatchingState_WithValidConnection_WithIdToken(
+                        idToken, rsaJWK, baseUrl, cookies, false, true));
+        assertTrue(
+                exception.getMessage().contains("Exceeded configured input 
limit"),
+                "Expected an input-limit-exceeded error but got: " + 
exception.getMessage());
+    }
+
+    @Test
+    void extractCredentials_WithJwkSetHttpSizeLimitRaised_ValidatesToken() 
throws JOSEException {
+        // A raised JWK set size limit must still allow the JWK set to be 
retrieved and the token validated.
+        config = createConfig(Map.of("userInfoEnabled", true, 
"jwkSetHttpSizeLimit", 102400));
+
+        RSAKey rsaJWK = new RSAKeyGenerator(2048).keyID("123").generate();
+        AuthenticationInfo authInfo = 
extractCredentials_WithMatchingState_WithValidConnection_WithIdToken(
+                createIdToken(rsaJWK, "client-id", ISSUER),
+                rsaJWK,
+                "http://localhost:"; + idpServer.getAddress().getPort(),
+                createMockCookies(),
+                false,
+                true);
+        assertEquals("1234567890", authInfo.get("user.name"));
+    }
+
+    @Test
+    void activation_WithZeroJwkSetHttpSizeLimit_FailsFast() {
+        // 0 means "unlimited" to Nimbus and must be rejected at activation, 
not silently accepted.
+        config = createConfig(Map.of("jwkSetHttpSizeLimit", 0));
+        IllegalArgumentException exception =
+                assertThrows(IllegalArgumentException.class, 
this::createOidcAuthenticationHandler);
+        assertTrue(
+                exception.getMessage().contains("jwkSetHttpSizeLimit"),
+                "Expected a validation error naming the property but got: " + 
exception.getMessage());
+    }
+
+    @Test
+    void activation_WithNegativeJwkSetHttpConnectTimeout_FailsFast() {
+        config = createConfig(Map.of("jwkSetHttpConnectTimeout", -1));
+        IllegalArgumentException exception =
+                assertThrows(IllegalArgumentException.class, 
this::createOidcAuthenticationHandler);
+        assertTrue(
+                exception.getMessage().contains("jwkSetHttpConnectTimeout"),
+                "Expected a validation error naming the property but got: " + 
exception.getMessage());
+    }
+
+    @Test
+    void activation_WithJwkSetHttpReadTimeoutAboveMax_FailsFast() {
+        config = createConfig(Map.of("jwkSetHttpReadTimeout", 60_001));
+        IllegalArgumentException exception =
+                assertThrows(IllegalArgumentException.class, 
this::createOidcAuthenticationHandler);
+        assertTrue(
+                exception.getMessage().contains("jwkSetHttpReadTimeout"),
+                "Expected a validation error naming the property but got: " + 
exception.getMessage());
+    }
+
     @Test
     void 
extractCredentials_WithMatchingState_WithValidConnection_WithValidIdToken_WithMissingUserInfo()
             throws JOSEException {

Reply via email to