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

oscerd pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new aa1907da380c CAMEL-26319: camel-oauth: InMemorySessionStore should not 
log session identifiers and OAuthCodeFlowCallback should rotate session 
identifiers on login (#28512)
aa1907da380c is described below

commit aa1907da380cb9ef87052fc6a050c87e029f7be1
Author: Andrea Cosentino <[email protected]>
AuthorDate: Wed Oct 28 11:13:49 2026 +0100

    CAMEL-26319: camel-oauth: InMemorySessionStore should not log session 
identifiers and OAuthCodeFlowCallback should rotate session identifiers on 
login (#28512)
    
    * CAMEL-26319: camel-oauth - remove session identifier logging and rotate 
session on login
    
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    * CAMEL-26319: camel-oauth - address review feedback on session rotation 
and logging
    
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    * CAMEL-26319: camel-oauth: address review feedback on session storage and 
logging
    
    - InMemorySessionStore: use ConcurrentHashMap for sessions to ensure thread 
safety
    - InMemorySessionStore: omit session identifiers from debug logs on 
creation and cookie setting
    - InMemorySessionStore: preserve duplicate Set-Cookie guard except for 
CAMEL_OAUTH_COOKIE replacement
    - Wrap upgrade guide entry at 120 characters
    
    Co-authored-by: Claude Opus 4.8 <[email protected]>
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    ---------
    
    Signed-off-by: Andrea Cosentino <[email protected]>
    Co-authored-by: Claude Opus 4.8 <[email protected]>
---
 .../org/apache/camel/oauth/InMemorySession.java    |  10 ++
 .../apache/camel/oauth/InMemorySessionStore.java   |  42 ++++--
 .../main/java/org/apache/camel/oauth/OAuth.java    |  11 ++
 .../apache/camel/oauth/OAuthCodeFlowCallback.java  |   1 +
 .../org/apache/camel/oauth/OAuthSessionStore.java  |  16 +++
 .../oauth/AbstractOAuthRequestHeadersTest.java     |   4 +
 .../oauth/OAuthCodeFlowSessionRotationTest.java    | 150 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |   5 +
 8 files changed, 231 insertions(+), 8 deletions(-)

diff --git 
a/components/camel-oauth/src/main/java/org/apache/camel/oauth/InMemorySession.java
 
b/components/camel-oauth/src/main/java/org/apache/camel/oauth/InMemorySession.java
index 7f96495aa515..373b57db66c1 100644
--- 
a/components/camel-oauth/src/main/java/org/apache/camel/oauth/InMemorySession.java
+++ 
b/components/camel-oauth/src/main/java/org/apache/camel/oauth/InMemorySession.java
@@ -34,6 +34,16 @@ public class InMemorySession implements OAuthSession {
         this.sessionId = sessionId;
     }
 
+    Map<String, Object> getValues() {
+        return new HashMap<>(values);
+    }
+
+    void putAll(Map<String, Object> newValues) {
+        if (newValues != null) {
+            this.values.putAll(newValues);
+        }
+    }
+
     @Override
     public String getSessionId() {
         return sessionId;
diff --git 
a/components/camel-oauth/src/main/java/org/apache/camel/oauth/InMemorySessionStore.java
 
b/components/camel-oauth/src/main/java/org/apache/camel/oauth/InMemorySessionStore.java
index 70c10047455c..7b6ee0e809e0 100644
--- 
a/components/camel-oauth/src/main/java/org/apache/camel/oauth/InMemorySessionStore.java
+++ 
b/components/camel-oauth/src/main/java/org/apache/camel/oauth/InMemorySessionStore.java
@@ -17,10 +17,10 @@
 package org.apache.camel.oauth;
 
 import java.util.Arrays;
-import java.util.HashMap;
 import java.util.Map;
 import java.util.Optional;
 import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.stream.Collectors;
 
 import org.apache.camel.Exchange;
@@ -35,7 +35,7 @@ public class InMemorySessionStore implements 
OAuthSessionStore {
 
     protected final Logger log = LoggerFactory.getLogger(getClass());
 
-    private final Map<String, OAuthSession> sessions = new HashMap<>();
+    private final Map<String, OAuthSession> sessions = new 
ConcurrentHashMap<>();
 
     @Override
     public Optional<OAuthSession> getSession(Exchange exchange) {
@@ -56,12 +56,13 @@ public class InMemorySessionStore implements 
OAuthSessionStore {
 
         var maybeSession = Optional.ofNullable(sessions.get(sessionId));
         if (maybeSession.isEmpty()) {
-            log.warn("No OAuthSession for: {}", sessionId);
+            log.warn("No OAuthSession found for cookie");
         }
 
         return maybeSession;
     }
 
+    @Override
     public OAuthSession createSession(Exchange exchange) {
 
         var sessionId = UUID.randomUUID().toString();
@@ -69,7 +70,7 @@ public class InMemorySessionStore implements 
OAuthSessionStore {
         sessions.put(sessionId, session);
 
         var msg = exchange.getMessage();
-        log.info("New OAuthSession: {}", sessionId);
+        log.debug("Created OAuthSession");
 
         setSessionCookie(msg, session);
         msg.setHeader(OAuth.CAMEL_OAUTH_SESSION_ID, sessionId);
@@ -77,6 +78,30 @@ public class InMemorySessionStore implements 
OAuthSessionStore {
         return session;
     }
 
+    @Override
+    public OAuthSession rotateSession(Exchange exchange, OAuthSession session) 
{
+        if (session == null) {
+            return createSession(exchange);
+        }
+        var oldSessionId = session.getSessionId();
+        sessions.remove(oldSessionId);
+
+        var newSessionId = UUID.randomUUID().toString();
+        var newSession = new InMemorySession(newSessionId);
+        if (session instanceof InMemorySession inMemorySession) {
+            newSession.putAll(inMemorySession.getValues());
+        }
+        sessions.put(newSessionId, newSession);
+
+        var msg = exchange.getMessage();
+        log.debug("Rotated OAuthSession");
+
+        setSessionCookie(msg, newSession);
+        msg.setHeader(OAuth.CAMEL_OAUTH_SESSION_ID, newSessionId);
+
+        return newSession;
+    }
+
     private Map<String, String> getCookies(Exchange exchange) {
 
         var msg = exchange.getMessage();
@@ -108,13 +133,14 @@ public class InMemorySessionStore implements 
OAuthSessionStore {
     }
 
     private void setSessionCookie(Message msg, OAuthSession session) {
-        var sessionId = session.getSessionId();
-        var cookieId = "%s=%s".formatted(CAMEL_OAUTH_COOKIE, sessionId);
-        if (msg.getHeader("Set-Cookie") != null) {
+        var existing = msg.getHeader("Set-Cookie", String.class);
+        if (existing != null && !existing.startsWith(CAMEL_OAUTH_COOKIE + 
"=")) {
             throw new IllegalStateException("Duplicate 'Set-Cookie' header");
         }
+        var sessionId = session.getSessionId();
+        var cookieId = "%s=%s".formatted(CAMEL_OAUTH_COOKIE, sessionId);
         var cookie = cookieId + "; Path=/; HttpOnly; SameSite=None; Secure";
         msg.setHeader("Set-Cookie", cookie);
-        log.debug("Set-Cookie: {}", cookie);
+        log.debug("Set {} cookie", CAMEL_OAUTH_COOKIE);
     }
 }
diff --git 
a/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuth.java 
b/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuth.java
index 3d2ce2583b98..54f6ac4ad787 100644
--- a/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuth.java
+++ b/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuth.java
@@ -98,4 +98,15 @@ public abstract class OAuth {
     public OAuthSession createSession(Exchange exchange) {
         return getSessionStore().createSession(exchange);
     }
+
+    /**
+     * Rotates the session, delegating to {@link 
OAuthSessionStore#rotateSession(Exchange, OAuthSession)}.
+     *
+     * @param  exchange the current exchange
+     * @param  session  the existing session to rotate
+     * @return          the rotated session with a new session identifier
+     */
+    public OAuthSession rotateSession(Exchange exchange, OAuthSession session) 
{
+        return getSessionStore().rotateSession(exchange, session);
+    }
 }
diff --git 
a/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuthCodeFlowCallback.java
 
b/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuthCodeFlowCallback.java
index c80eda2a7763..813387395cfa 100644
--- 
a/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuthCodeFlowCallback.java
+++ 
b/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuthCodeFlowCallback.java
@@ -76,6 +76,7 @@ public class OAuthCodeFlowCallback extends 
AbstractOAuthProcessor {
                 .setRedirectUri(redirectUri)
                 .setCode(authCode));
 
+        session = oauth.rotateSession(exchange, session);
         session.putUserProfile(userProfile);
         log.info("Authenticated {}", userProfile.subject());
         userProfile.logDetails();
diff --git 
a/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuthSessionStore.java
 
b/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuthSessionStore.java
index 4220f2c4f88d..9da5928cf928 100644
--- 
a/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuthSessionStore.java
+++ 
b/components/camel-oauth/src/main/java/org/apache/camel/oauth/OAuthSessionStore.java
@@ -25,4 +25,20 @@ public interface OAuthSessionStore {
     Optional<OAuthSession> getSession(Exchange exchange);
 
     OAuthSession createSession(Exchange exchange);
+
+    /**
+     * Rotates the session, replacing the current session identifier with a 
newly generated one and transferring any
+     * session state.
+     *
+     * @implNote          The default implementation is a no-op that returns 
the existing session unchanged.
+     *                    Implementors MUST override this method to prevent 
session fixation — failing to do so leaves
+     *                    the session identifier unchanged across privilege 
escalation (login).
+     *
+     * @param    exchange the current exchange
+     * @param    session  the existing session to rotate
+     * @return            the rotated session with a new session identifier
+     */
+    default OAuthSession rotateSession(Exchange exchange, OAuthSession 
session) {
+        return session;
+    }
 }
diff --git 
a/components/camel-oauth/src/test/java/org/apache/camel/oauth/AbstractOAuthRequestHeadersTest.java
 
b/components/camel-oauth/src/test/java/org/apache/camel/oauth/AbstractOAuthRequestHeadersTest.java
index 2fa2eeb6643f..8b34ea4451b2 100644
--- 
a/components/camel-oauth/src/test/java/org/apache/camel/oauth/AbstractOAuthRequestHeadersTest.java
+++ 
b/components/camel-oauth/src/test/java/org/apache/camel/oauth/AbstractOAuthRequestHeadersTest.java
@@ -213,6 +213,10 @@ abstract class AbstractOAuthRequestHeadersTest {
         var callback = get("/auth?code=" + StubOAuth.BOB_CODE + "&state=" + 
URLEncoder.encode(state, StandardCharsets.UTF_8),
                 cookie, null);
         assertEquals(302, callback.statusCode());
+        var postLoginCookie = setCookie(callback);
+        if (postLoginCookie != null) {
+            cookie = postLoginCookie.substring(0, 
postLoginCookie.indexOf(';'));
+        }
         return cookie;
     }
 
diff --git 
a/components/camel-oauth/src/test/java/org/apache/camel/oauth/OAuthCodeFlowSessionRotationTest.java
 
b/components/camel-oauth/src/test/java/org/apache/camel/oauth/OAuthCodeFlowSessionRotationTest.java
new file mode 100644
index 000000000000..e2c099fff54c
--- /dev/null
+++ 
b/components/camel-oauth/src/test/java/org/apache/camel/oauth/OAuthCodeFlowSessionRotationTest.java
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.camel.oauth;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.support.DefaultExchange;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.apache.camel.oauth.OAuth.CAMEL_OAUTH_COOKIE;
+import static org.apache.camel.oauth.OAuth.CAMEL_OAUTH_REDIRECT_URI;
+import static org.apache.camel.oauth.OAuth.CAMEL_OAUTH_SESSION_ID;
+import static org.apache.camel.oauth.OAuthSession.OAUTH_STATE;
+import static org.assertj.core.api.Assertions.assertThat;
+
+class OAuthCodeFlowSessionRotationTest {
+
+    private static final String REDIRECT_URI = "https://app.example.test/auth";;
+    private static final String POST_LOGIN_URL = 
"https://app.example.test/dashboard";;
+
+    private DefaultCamelContext context;
+    private StubOAuth oauth;
+
+    @BeforeEach
+    void setUp() {
+        context = new DefaultCamelContext();
+        
context.getPropertiesComponent().addInitialProperty(CAMEL_OAUTH_REDIRECT_URI, 
REDIRECT_URI);
+        oauth = new StubOAuth();
+        context.getRegistry().bind(OAuth.class.getName(), oauth);
+    }
+
+    @AfterEach
+    void tearDown() throws Exception {
+        context.close();
+    }
+
+    @Test
+    void inMemorySessionStoreRotateSessionReplacesIdAndTransfersValues() {
+        Exchange exchange = new DefaultExchange(context);
+        OAuthSession initialSession = oauth.createSession(exchange);
+        String oldSessionId = initialSession.getSessionId();
+
+        initialSession.putValue("customKey", "customValue");
+        initialSession.putValue("anotherKey", 12345);
+
+        OAuthSession rotatedSession = oauth.rotateSession(exchange, 
initialSession);
+        String newSessionId = rotatedSession.getSessionId();
+
+        assertThat(newSessionId).isNotEqualTo(oldSessionId);
+        assertThat(rotatedSession.getValue("customKey", 
String.class)).contains("customValue");
+        assertThat(rotatedSession.getValue("anotherKey", 
Integer.class)).contains(12345);
+
+        // Verify the old session is no longer accessible in the store
+        Exchange oldLookupExchange = new DefaultExchange(context);
+        oldLookupExchange.getMessage().setHeader(CAMEL_OAUTH_SESSION_ID, 
oldSessionId);
+        assertThat(oauth.getSession(oldLookupExchange)).isEmpty();
+
+        // Verify the new session is retrievable
+        Exchange newLookupExchange = new DefaultExchange(context);
+        newLookupExchange.getMessage().setHeader(CAMEL_OAUTH_SESSION_ID, 
newSessionId);
+        
assertThat(oauth.getSession(newLookupExchange)).containsSame(rotatedSession);
+
+        // Verify the exchange response cookie and header were updated
+        String setCookieHeader = exchange.getMessage().getHeader("Set-Cookie", 
String.class);
+        assertThat(setCookieHeader)
+                .isNotNull()
+                .contains(CAMEL_OAUTH_COOKIE + "=" + newSessionId)
+                .doesNotContain(oldSessionId);
+        
assertThat(exchange.getMessage().getHeader(CAMEL_OAUTH_SESSION_ID)).isEqualTo(newSessionId);
+    }
+
+    @Test
+    void callbackRotatesSessionUponSuccessfulAuthentication() throws Exception 
{
+        // Step 1: Pre-login initial request creates a pre-login session
+        Exchange preLoginExchange = new DefaultExchange(context);
+        OAuthSession preLoginSession = oauth.createSession(preLoginExchange);
+        String preLoginSessionId = preLoginSession.getSessionId();
+
+        String issuedState = "secure-random-state-1234";
+        preLoginSession.putValue(OAUTH_STATE, issuedState);
+        preLoginSession.putValue("OAuthPostLoginUrl", POST_LOGIN_URL);
+
+        // Step 2: Callback completes with valid authorization code and state
+        Exchange callbackExchange = new DefaultExchange(context);
+        callbackExchange.getMessage().setHeader("Cookie", CAMEL_OAUTH_COOKIE + 
"=" + preLoginSessionId);
+        callbackExchange.getMessage().setHeader("code", StubOAuth.BOB_CODE);
+        callbackExchange.getMessage().setHeader("state", issuedState);
+
+        new OAuthCodeFlowCallback().process(callbackExchange);
+
+        // Verify redirect to post-login URL
+        
assertThat(callbackExchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isEqualTo(302);
+        
assertThat(callbackExchange.getMessage().getHeader("Location")).isEqualTo(POST_LOGIN_URL);
+
+        // Verify new session cookie is emitted with rotated session ID
+        String setCookieHeader = 
callbackExchange.getMessage().getHeader("Set-Cookie", String.class);
+        assertThat(setCookieHeader).isNotNull();
+        assertThat(setCookieHeader).doesNotContain(preLoginSessionId);
+
+        String prefix = CAMEL_OAUTH_COOKIE + "=";
+        int start = setCookieHeader.indexOf(prefix) + prefix.length();
+        int end = setCookieHeader.indexOf(';', start);
+        String postLoginSessionId = end > 0 ? setCookieHeader.substring(start, 
end) : setCookieHeader.substring(start);
+
+        assertThat(postLoginSessionId).isNotEmpty();
+        assertThat(postLoginSessionId).isNotEqualTo(preLoginSessionId);
+
+        // Step 3: Verify the pre-login session ID cannot be reused to access 
protected routes
+        Exchange replayExchange = new DefaultExchange(context);
+        replayExchange.getMessage().setHeader("Cookie", CAMEL_OAUTH_COOKIE + 
"=" + preLoginSessionId);
+        new OAuthCodeFlowProcessor().process(replayExchange);
+
+        // The pre-login ID should not be authenticated, so 
OAuthCodeFlowProcessor redirects to IdP auth request
+        
assertThat(replayExchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isEqualTo(302);
+        assertThat(replayExchange.getMessage().getHeader("Location", 
String.class)).contains("https://idp.example/auth?state=";);
+        assertThat(replayExchange.isRouteStop()).isTrue();
+
+        // Step 4: Verify the rotated post-login session ID successfully 
authenticates the user
+        Exchange authExchange = new DefaultExchange(context);
+        authExchange.getMessage().setHeader("Cookie", CAMEL_OAUTH_COOKIE + "=" 
+ postLoginSessionId);
+        new OAuthCodeFlowProcessor().process(authExchange);
+
+        // The authenticated request should NOT be redirected and route should 
not be stopped
+        
assertThat(authExchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isNull();
+        assertThat(authExchange.isRouteStop()).isFalse();
+
+        // Stored session has bob's profile
+        Exchange verifyExchange = new DefaultExchange(context);
+        verifyExchange.getMessage().setHeader(CAMEL_OAUTH_SESSION_ID, 
postLoginSessionId);
+        OAuthSession storedSession = 
oauth.getSession(verifyExchange).orElseThrow();
+        assertThat(storedSession.getUserProfile()).isPresent();
+        
assertThat(storedSession.getUserProfile().get().subject()).isEqualTo("bob");
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 9713eb394d3c..4087672a4b22 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -1627,6 +1627,11 @@ with `OAuthHelper.validateOAuthToken` from camel-support 
instead.
 For any other message, such as one from camel-undertow, the processors keep 
reading the message headers, but leave out
 a value that also appears in the query string of the request.
 
+`InMemorySessionStore` no longer logs session identifiers. Additionally, 
`OAuthCodeFlowCallback` rotates the session
+identifier upon successful authentication, replacing the pre-login session 
identifier with a new session identifier
+and emitting an updated session cookie. Custom `OAuthSessionStore` 
implementations can override the default
+`rotateSession` method to support session rotation on login.
+
 === camel-platform-http-vertx - the charset parameter of the request 
Content-Type in any case
 
 The consumer now reads the charset parameter of the request `Content-Type` 
whatever the case of its name, as

Reply via email to