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