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 091488d  SLING-13048 Enhance OIDC authentication flow by adding 
configurable request key cookie max age (#43)
091488d is described below

commit 091488d7a57047934039784fadf8417f32735910
Author: Nicola Scendoni <[email protected]>
AuthorDate: Tue Feb 10 16:56:17 2026 +0100

    SLING-13048 Enhance OIDC authentication flow by adding configurable request 
key cookie max age (#43)
    
    * SLING-13048 Enhance OIDC authentication flow by adding configurable 
request key cookie max age
    
    - Introduced a new configuration option for the request key cookie max age 
in OidcAuthenticationHandler.
    - Updated RedirectHelper to utilize the configurable max age when setting 
cookies.
    - Modified OAuthEntryPointServlet to pass the new max age parameter.
    - Added tests to verify the correct usage of the configured cookie max age.
    
    * Refactor RedirectHelperTest to use a constant for cookie expiration time
    
    - Introduced a constant COOKIE_EXPIRE_SECONDS to replace hardcoded values 
in test cases.
    - Updated all relevant test methods to utilize the new constant for 
improved readability and maintainability.
    
    * Reformatted method calls in RedirectHelperTest
    
    * Enhance OAuthEntryPointServlet with configurable request key cookie max 
age
    
    - Added a new configuration option for the request key cookie max age in 
OAuthEntryPointServlet.
    - Updated the constructor to accept the configuration and utilize the 
specified max age for the request key cookie.
    - Enhanced unit tests to verify the correct application of the configured 
cookie max age.
---
 .../oauth_client/impl/OAuthEntryPointServlet.java  | 31 +++++++++++-
 .../impl/OidcAuthenticationHandler.java            | 14 ++++-
 .../auth/oauth_client/impl/RedirectHelper.java     | 17 +++++--
 .../impl/OAuthEntryPointServletTest.java           | 29 ++++++++++-
 .../impl/OidcAuthenticationHandlerTest.java        |  1 +
 .../auth/oauth_client/impl/RedirectHelperTest.java | 59 ++++++++++++++++++++--
 6 files changed, 137 insertions(+), 14 deletions(-)

diff --git 
a/src/main/java/org/apache/sling/auth/oauth_client/impl/OAuthEntryPointServlet.java
 
b/src/main/java/org/apache/sling/auth/oauth_client/impl/OAuthEntryPointServlet.java
index fcda20b..aef6e1b 100644
--- 
a/src/main/java/org/apache/sling/auth/oauth_client/impl/OAuthEntryPointServlet.java
+++ 
b/src/main/java/org/apache/sling/auth/oauth_client/impl/OAuthEntryPointServlet.java
@@ -40,6 +40,9 @@ import org.jetbrains.annotations.NotNull;
 import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -48,6 +51,7 @@ import static 
org.osgi.service.component.annotations.ReferencePolicyOption.GREED
 @Component(
         service = {Servlet.class},
         property = {AuthConstants.AUTH_REQUIREMENTS + "=" + 
OAuthEntryPointServlet.PATH})
+@Designate(ocd = OAuthEntryPointServlet.Config.class)
 @SlingServletPaths(OAuthEntryPointServlet.PATH)
 public class OAuthEntryPointServlet extends SlingAllMethodsServlet {
 
@@ -61,12 +65,29 @@ public class OAuthEntryPointServlet extends 
SlingAllMethodsServlet {
 
     private final CryptoService cryptoService;
 
+    private final int requestKeyCookieMaxAgeSeconds;
+
+    @ObjectClassDefinition(
+            name = "Apache Sling OAuth Entry Point Servlet",
+            description = "OAuth entry point servlet for initiating the OAuth 
authentication flow.")
+    @interface Config {
+        @AttributeDefinition(
+                name = "Request key cookie max age (seconds)",
+                description = "Max age in seconds for the 
sling.oauth-request-key cookie used during the OAuth "
+                        + "authentication flow. The cookie holds state between 
the redirect to the IdP and the "
+                        + "callback. Default is 300 (5 minutes). Use a higher 
value if users often take longer "
+                        + "(e.g. consent, 2FA).")
+        int requestKeyCookieMaxAgeSeconds() default 
RedirectHelper.DEFAULT_REQUEST_KEY_COOKIE_MAX_AGE_SECONDS;
+    }
+
     @Activate
     public OAuthEntryPointServlet(
             @Reference(policyOption = GREEDY) List<ClientConnection> 
connections,
-            @Reference CryptoService cryptoService) {
+            @Reference CryptoService cryptoService,
+            Config config) {
         this.connections = 
connections.stream().collect(Collectors.toMap(ClientConnection::name, 
Function.identity()));
         this.cryptoService = cryptoService;
+        this.requestKeyCookieMaxAgeSeconds = 
config.requestKeyCookieMaxAgeSeconds();
     }
 
     @Override
@@ -116,6 +137,12 @@ public class OAuthEntryPointServlet extends 
SlingAllMethodsServlet {
         OAuthCookieValue oAuthCookieValue = new 
OAuthCookieValue(perRequestKey, connection.name(), redirect);
 
         return RedirectHelper.buildRedirectTarget(
-                new String[] {PATH}, callbackUri, conn, oAuthCookieValue, 
cryptoService, null);
+                new String[] {PATH},
+                callbackUri,
+                conn,
+                oAuthCookieValue,
+                cryptoService,
+                null,
+                requestKeyCookieMaxAgeSeconds);
     }
 }
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 9eb31c7..b5b9174 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
@@ -111,6 +111,8 @@ public class OidcAuthenticationHandler extends 
DefaultAuthenticationFeedbackHand
 
     private final String[] resource;
 
+    private final int requestKeyCookieMaxAgeSeconds;
+
     private final CryptoService cryptoService;
 
     @ObjectClassDefinition(
@@ -148,6 +150,14 @@ public class OidcAuthenticationHandler extends 
DefaultAuthenticationFeedbackHand
                         + "Multiple values can be specified.",
                 cardinality = Integer.MAX_VALUE)
         String[] resource() default {};
+
+        @AttributeDefinition(
+                name = "Request key cookie max age (seconds)",
+                description = "Max age in seconds for the 
sling.oauth-request-key cookie used during the OIDC "
+                        + "authentication flow. The cookie holds state between 
the redirect to the IdP and the "
+                        + "callback. Default is 300 (5 minutes). Use a higher 
value if users often take longer "
+                        + "(e.g. consent, 2FA).")
+        int requestKeyCookieMaxAgeSeconds() default 
RedirectHelper.DEFAULT_REQUEST_KEY_COOKIE_MAX_AGE_SECONDS;
     }
 
     @Activate
@@ -170,6 +180,7 @@ public class OidcAuthenticationHandler extends 
DefaultAuthenticationFeedbackHand
         this.pkceEnabled = config.pkceEnabled();
         this.path = config.path();
         this.resource = config.resource();
+        this.requestKeyCookieMaxAgeSeconds = 
config.requestKeyCookieMaxAgeSeconds();
         this.cryptoService = cryptoService;
 
         logger.debug("activate: registering ExternalIdentityProvider");
@@ -548,7 +559,8 @@ public class OidcAuthenticationHandler extends 
DefaultAuthenticationFeedbackHand
         OAuthCookieValue oAuthCookieValue =
                 new OAuthCookieValue(perRequestKey, connection.name(), 
redirect, nonce, codeVerifier);
 
-        return RedirectHelper.buildRedirectTarget(path, callbackUri, conn, 
oAuthCookieValue, cryptoService, resource);
+        return RedirectHelper.buildRedirectTarget(
+                path, callbackUri, conn, oAuthCookieValue, cryptoService, 
resource, requestKeyCookieMaxAgeSeconds);
     }
 
     @Override
diff --git 
a/src/main/java/org/apache/sling/auth/oauth_client/impl/RedirectHelper.java 
b/src/main/java/org/apache/sling/auth/oauth_client/impl/RedirectHelper.java
index 7a7f2af..e62bb8d 100644
--- a/src/main/java/org/apache/sling/auth/oauth_client/impl/RedirectHelper.java
+++ b/src/main/java/org/apache/sling/auth/oauth_client/impl/RedirectHelper.java
@@ -44,7 +44,9 @@ class RedirectHelper {
     // We don't want leave the cookie lying around for a long time because it 
is not needed.
     // At the same time, some OAuth user authentication flows take a long time 
due to
     // consent, account selection, 2FA, etc. so we cannot make this too short.
-    private static final int COOKIE_MAX_AGE_SECONDS = 300;
+    /** Default max age in seconds for the sling.oauth-request-key cookie. 
Used when not configured by the handler. */
+    public static final int DEFAULT_REQUEST_KEY_COOKIE_MAX_AGE_SECONDS = 300;
+
     private static final Logger logger = 
LoggerFactory.getLogger(RedirectHelper.class);
 
     private RedirectHelper() {
@@ -57,13 +59,17 @@ class RedirectHelper {
             @NotNull ResolvedConnection conn,
             @NotNull OAuthCookieValue oAuthCookieValue,
             @NotNull CryptoService cryptoService,
-            @Nullable String[] resource) {
+            @Nullable String[] resource,
+            int requestKeyCookieMaxAgeSeconds) {
 
         String path = findLongestPathMatching(paths, callbackUri.getPath());
 
         // Set the cookie with state, connection name, redirect uri, nonce and 
codeverifier
         Cookie requestKeyCookie = buildCookie(
-                path, OAuthCookieValue.COOKIE_NAME_REQUEST_KEY, 
cryptoService.encrypt(oAuthCookieValue.getValue()));
+                path,
+                OAuthCookieValue.COOKIE_NAME_REQUEST_KEY,
+                cryptoService.encrypt(oAuthCookieValue.getValue()),
+                requestKeyCookieMaxAgeSeconds);
 
         // We build th redirect url to be sent to the browser
         URI authorizationEndpointUri = 
URI.create(conn.authorizationEndpoint());
@@ -111,11 +117,12 @@ class RedirectHelper {
         return new RedirectTarget(uri, requestKeyCookie);
     }
 
-    private static @NotNull Cookie buildCookie(@Nullable String path, @NotNull 
String name, @NotNull String value) {
+    private static @NotNull Cookie buildCookie(
+            @Nullable String path, @NotNull String name, @NotNull String 
value, int maxAgeSeconds) {
         Cookie cookie = new Cookie(name, value);
         cookie.setHttpOnly(true);
         cookie.setSecure(true);
-        cookie.setMaxAge(COOKIE_MAX_AGE_SECONDS);
+        cookie.setMaxAge(maxAgeSeconds);
         if (path != null) cookie.setPath(path);
         return cookie;
     }
diff --git 
a/src/test/java/org/apache/sling/auth/oauth_client/impl/OAuthEntryPointServletTest.java
 
b/src/test/java/org/apache/sling/auth/oauth_client/impl/OAuthEntryPointServletTest.java
index dc37de6..b5a1cac 100644
--- 
a/src/test/java/org/apache/sling/auth/oauth_client/impl/OAuthEntryPointServletTest.java
+++ 
b/src/test/java/org/apache/sling/auth/oauth_client/impl/OAuthEntryPointServletTest.java
@@ -19,6 +19,7 @@
 package org.apache.sling.auth.oauth_client.impl;
 
 import javax.servlet.ServletException;
+import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletResponse;
 
 import java.io.IOException;
@@ -36,7 +37,10 @@ import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 @ExtendWith(SlingContextExtension.class)
 class OAuthEntryPointServletTest {
@@ -57,7 +61,10 @@ class OAuthEntryPointServletTest {
                         "client-secret",
                         "http://example.com";,
                         new String[] {"access_type=offline"}));
-        servlet = new OAuthEntryPointServlet(connections, new 
StubCryptoService());
+        OAuthEntryPointServlet.Config config = 
mock(OAuthEntryPointServlet.Config.class);
+        when(config.requestKeyCookieMaxAgeSeconds())
+                
.thenReturn(RedirectHelper.DEFAULT_REQUEST_KEY_COOKIE_MAX_AGE_SECONDS);
+        servlet = new OAuthEntryPointServlet(connections, new 
StubCryptoService(), config);
     }
 
     @Test
@@ -190,4 +197,24 @@ class OAuthEntryPointServletTest {
 
         assertThat(context.response().getStatus()).as("response 
code").isEqualTo(HttpServletResponse.SC_BAD_REQUEST);
     }
+
+    @Test
+    void usesConfiguredCookieMaxAge() throws ServletException, IOException {
+        int customMaxAge = 600;
+        OAuthEntryPointServlet.Config config = 
mock(OAuthEntryPointServlet.Config.class);
+        when(config.requestKeyCookieMaxAgeSeconds()).thenReturn(customMaxAge);
+        OAuthEntryPointServlet servletWithCustomConfig = new 
OAuthEntryPointServlet(
+                Arrays.asList(MockOidcConnection.DEFAULT_CONNECTION), new 
StubCryptoService(), config);
+
+        context.request().setQueryString("c=" + 
MockOidcConnection.DEFAULT_CONNECTION.name());
+        MockSlingHttpServletResponse response = context.response();
+
+        servletWithCustomConfig.service(context.request(), response);
+
+        Cookie requestKeyCookie = Arrays.stream(response.getCookies())
+                .filter(c -> 
OAuthCookieValue.COOKIE_NAME_REQUEST_KEY.equals(c.getName()))
+                .findFirst()
+                .orElseThrow();
+        assertEquals(customMaxAge, requestKeyCookie.getMaxAge());
+    }
 }
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 f257fc8..b07254f 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
@@ -102,6 +102,7 @@ class OidcAuthenticationHandlerTest {
         config = mock(OidcAuthenticationHandler.Config.class);
         when(config.idp()).thenReturn("myIdP");
         when(config.path()).thenReturn(new String[] {"/"});
+        when(config.requestKeyCookieMaxAgeSeconds()).thenReturn(300);
         loginCookieManager = mock(LoginCookieManager.class);
 
         SlingUserInfoProcessorImpl.Config userInfoConfig = 
Converters.standardConverter()
diff --git 
a/src/test/java/org/apache/sling/auth/oauth_client/impl/RedirectHelperTest.java 
b/src/test/java/org/apache/sling/auth/oauth_client/impl/RedirectHelperTest.java
index ef4266b..0bc6420 100644
--- 
a/src/test/java/org/apache/sling/auth/oauth_client/impl/RedirectHelperTest.java
+++ 
b/src/test/java/org/apache/sling/auth/oauth_client/impl/RedirectHelperTest.java
@@ -33,6 +33,8 @@ import static org.mockito.Mockito.when;
 
 class RedirectHelperTest {
 
+    final int COOKIE_EXPIRE_SECONDS = 300;
+
     @Test
     void testFindLongestPathMatchingWithValidPaths() {
         String[] paths = {"/a/b/c", "/a/b", "/a"};
@@ -151,7 +153,13 @@ class RedirectHelperTest {
         String[] audience = new String[] {"https://api.example.com"};
 
         RedirectTarget result = RedirectHelper.buildRedirectTarget(
-                new String[] {"/"}, URI.create("/callback"), conn, 
oAuthCookieValue, cryptoService, audience);
+                new String[] {"/"},
+                URI.create("/callback"),
+                conn,
+                oAuthCookieValue,
+                cryptoService,
+                audience,
+                COOKIE_EXPIRE_SECONDS);
 
         assertNotNull(result);
         assertNotNull(result.uri());
@@ -170,7 +178,13 @@ class RedirectHelperTest {
         String[] audience = new String[] {"https://api1.example.com";, 
"https://api2.example.com"};
 
         RedirectTarget result = RedirectHelper.buildRedirectTarget(
-                new String[] {"/"}, URI.create("/callback"), conn, 
oAuthCookieValue, cryptoService, audience);
+                new String[] {"/"},
+                URI.create("/callback"),
+                conn,
+                oAuthCookieValue,
+                cryptoService,
+                audience,
+                COOKIE_EXPIRE_SECONDS);
 
         assertNotNull(result);
         assertNotNull(result.uri());
@@ -193,7 +207,13 @@ class RedirectHelperTest {
         String[] audience = new String[] {};
 
         RedirectTarget result = RedirectHelper.buildRedirectTarget(
-                new String[] {"/"}, URI.create("/callback"), conn, 
oAuthCookieValue, cryptoService, audience);
+                new String[] {"/"},
+                URI.create("/callback"),
+                conn,
+                oAuthCookieValue,
+                cryptoService,
+                audience,
+                COOKIE_EXPIRE_SECONDS);
 
         assertRedirectTargetHasNoResourceParameter(result);
     }
@@ -206,7 +226,13 @@ class RedirectHelperTest {
                 new OAuthCookieValue("perRequestKey", "connectionName", 
"/redirect", new Nonce("nonce"), null);
 
         RedirectTarget result = RedirectHelper.buildRedirectTarget(
-                new String[] {"/"}, URI.create("/callback"), conn, 
oAuthCookieValue, cryptoService, null);
+                new String[] {"/"},
+                URI.create("/callback"),
+                conn,
+                oAuthCookieValue,
+                cryptoService,
+                null,
+                COOKIE_EXPIRE_SECONDS);
 
         assertRedirectTargetHasNoResourceParameter(result);
     }
@@ -221,7 +247,13 @@ class RedirectHelperTest {
         String[] audience = new String[] {"", "  ", "https://api.example.com";, 
null};
 
         RedirectTarget result = RedirectHelper.buildRedirectTarget(
-                new String[] {"/"}, URI.create("/callback"), conn, 
oAuthCookieValue, cryptoService, audience);
+                new String[] {"/"},
+                URI.create("/callback"),
+                conn,
+                oAuthCookieValue,
+                cryptoService,
+                audience,
+                COOKIE_EXPIRE_SECONDS);
 
         assertNotNull(result);
         assertNotNull(result.uri());
@@ -234,6 +266,23 @@ class RedirectHelperTest {
         assertEquals(1, count, "Expected exactly one resource parameter but 
found " + count);
     }
 
+    @Test
+    void testBuildRedirectTargetUsesConfiguredCookieMaxAge() {
+        ResolvedConnection conn = createMockResolvedConnection();
+        CryptoService cryptoService = new StubCryptoService();
+        OAuthCookieValue oAuthCookieValue =
+                new OAuthCookieValue("perRequestKey", "connectionName", 
"/redirect", new Nonce("nonce"), null);
+
+        int customMaxAge = 600;
+        RedirectTarget result = RedirectHelper.buildRedirectTarget(
+                new String[] {"/"}, URI.create("/callback"), conn, 
oAuthCookieValue, cryptoService, null, customMaxAge);
+
+        assertNotNull(result);
+        assertNotNull(result.cookie());
+        assertEquals(OAuthCookieValue.COOKIE_NAME_REQUEST_KEY, 
result.cookie().getName());
+        assertEquals(customMaxAge, result.cookie().getMaxAge());
+    }
+
     private ResolvedConnection createMockResolvedConnection() {
         ResolvedConnection conn = mock(ResolvedConnection.class);
         
when(conn.authorizationEndpoint()).thenReturn("http://localhost:8080/authorize";);

Reply via email to