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

Croway 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 6f72ae648eb0 CAMEL-24439: camel-shiro - verify the presented 
credentials on every exchange (#25822)
6f72ae648eb0 is described below

commit 6f72ae648eb0c6e54a913f7be33cd2c020355b95
Author: Andrea Cosentino <[email protected]>
AuthorDate: Fri Aug 28 12:10:49 2026 +0200

    CAMEL-24439: camel-shiro - verify the presented credentials on every 
exchange (#25822)
    
    * CAMEL-24439: camel-shiro - verify the presented credentials on every 
exchange
    
    ShiroSecurityProcessor.authenticateUser() called login() only when the 
thread-bound
    subject was not already authenticated for the same username as the incoming
    ShiroSecurityToken:
    
        if (!authenticated || !sameUser) { ... currentUser.login(token); }
    
    That conflates "same principal name" with "same credentials". Once a user 
had
    authenticated on a worker thread, a later exchange presenting that username 
with any
    password was accepted for as long as the subject stayed bound, because the 
password
    was never checked.
    
    The default alwaysReauthenticate=true masks it, since the processor calls 
logout() in a
    finally block after each exchange. With alwaysReauthenticate=false the skip 
is
    reachable, and that mode deliberately sets rememberMe(true) to keep subjects
    long-lived on Camel's shared worker threads.
    
    Call login() for every exchange with the credentials that exchange 
presented. Shiro
    offers no way to compare presented credentials against a bound subject, so 
the
    principal-name comparison could not be made sound and is removed rather 
than narrowed.
    
    The added test sends a valid token for ringo, then the same username with a 
wrong
    password on the same thread; without the fix both reach mock:success.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    * CAMEL-24439: Simplify test encryption key
    
    ---------
    
    Signed-off-by: Andrea Cosentino <[email protected]>
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
    Co-authored-by: Croway <[email protected]>
    Co-authored-by: Federico Mariani <[email protected]>
---
 .../shiro/security/ShiroSecurityProcessor.java     | 29 +++----
 ...oAuthenticationCredentialAlwaysCheckedTest.java | 93 ++++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    | 16 ++++
 3 files changed, 122 insertions(+), 16 deletions(-)

diff --git 
a/components/camel-shiro/src/main/java/org/apache/camel/component/shiro/security/ShiroSecurityProcessor.java
 
b/components/camel-shiro/src/main/java/org/apache/camel/component/shiro/security/ShiroSecurityProcessor.java
index 29b1271fa2c5..98f18bec1221 100644
--- 
a/components/camel-shiro/src/main/java/org/apache/camel/component/shiro/security/ShiroSecurityProcessor.java
+++ 
b/components/camel-shiro/src/main/java/org/apache/camel/component/shiro/security/ShiroSecurityProcessor.java
@@ -130,24 +130,21 @@ public class ShiroSecurityProcessor extends 
DelegateAsyncProcessor {
     }
 
     private void authenticateUser(Subject currentUser, ShiroSecurityToken 
securityToken) {
-        boolean authenticated = currentUser.isAuthenticated();
-        boolean sameUser = 
securityToken.getUsername().equals(currentUser.getPrincipal());
-        LOG.trace("Authenticated: {}, same Username: {}", authenticated, 
sameUser);
+        // The login is not skipped when the thread-bound subject already 
carries the same principal name.
+        // A matching username says nothing about the password presented with 
this exchange, and with
+        // alwaysReauthenticate=false the subject is deliberately long-lived 
on a shared worker thread, so
+        // skipping would let a wrong password through for as long as that 
subject stays bound.
+        LOG.trace("Authenticating {} (subject currently authenticated: {})",
+                securityToken.getUsername(), currentUser.isAuthenticated());
 
-        if (!authenticated || !sameUser) {
-            UsernamePasswordToken token = new 
UsernamePasswordToken(securityToken.getUsername(), securityToken.getPassword());
-            if (policy.isAlwaysReauthenticate()) {
-                token.setRememberMe(false);
-            } else {
-                token.setRememberMe(true);
-            }
+        UsernamePasswordToken token = new 
UsernamePasswordToken(securityToken.getUsername(), securityToken.getPassword());
+        token.setRememberMe(!policy.isAlwaysReauthenticate());
 
-            try {
-                currentUser.login(token);
-                LOG.debug("Current user {} successfully authenticated", 
currentUser.getPrincipal());
-            } catch (AuthenticationException ae) {
-                throw new AuthenticationException("Authentication Failed.", 
ae.getCause());
-            }
+        try {
+            currentUser.login(token);
+            LOG.debug("Current user {} successfully authenticated", 
currentUser.getPrincipal());
+        } catch (AuthenticationException ae) {
+            throw new AuthenticationException("Authentication Failed.", 
ae.getCause());
         }
     }
 
diff --git 
a/components/camel-shiro/src/test/java/org/apache/camel/component/shiro/security/ShiroAuthenticationCredentialAlwaysCheckedTest.java
 
b/components/camel-shiro/src/test/java/org/apache/camel/component/shiro/security/ShiroAuthenticationCredentialAlwaysCheckedTest.java
new file mode 100644
index 000000000000..7e32cc11db2b
--- /dev/null
+++ 
b/components/camel-shiro/src/test/java/org/apache/camel/component/shiro/security/ShiroAuthenticationCredentialAlwaysCheckedTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.component.shiro.security;
+
+import java.nio.charset.StandardCharsets;
+
+import org.apache.camel.EndpointInject;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.apache.shiro.authc.AuthenticationException;
+import org.apache.shiro.authc.IncorrectCredentialsException;
+import org.apache.shiro.authc.LockedAccountException;
+import org.apache.shiro.authc.UnknownAccountException;
+import org.junit.jupiter.api.Test;
+
+/**
+ * With {@code alwaysReauthenticate=false} the Shiro subject stays bound to 
the thread between exchanges, which is the
+ * point of the option. The password carried by each exchange still has to be 
verified: a username that happens to match
+ * the bound subject's principal is not evidence about the credentials 
presented this time.
+ */
+class ShiroAuthenticationCredentialAlwaysCheckedTest extends CamelTestSupport {
+
+    private static final byte[] TEST_KEY = 
"0123456789abcdef".getBytes(StandardCharsets.US_ASCII);
+
+    @EndpointInject("mock:success")
+    protected MockEndpoint successEndpoint;
+
+    @EndpointInject("mock:authenticationException")
+    protected MockEndpoint failureEndpoint;
+
+    @Test
+    void aWrongPasswordIsRejectedEvenAfterTheSameUserAuthenticated() throws 
Exception {
+        successEndpoint.expectedMessageCount(1);
+        failureEndpoint.expectedMessageCount(1);
+
+        // Authenticates and leaves the subject bound to this thread
+        template.send("direct:secureEndpoint", injector("ringo", "starr"));
+        // Same username, wrong password - must not ride the bound subject 
through
+        template.send("direct:secureEndpoint", injector("ringo", "not-starr"));
+
+        successEndpoint.assertIsSatisfied();
+        failureEndpoint.assertIsSatisfied();
+    }
+
+    private TestShiroSecurityTokenInjector injector(String user, String 
password) {
+        return new TestShiroSecurityTokenInjector(new ShiroSecurityToken(user, 
password), TEST_KEY);
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        final ShiroSecurityPolicy securityPolicy
+                = new 
ShiroSecurityPolicy("./src/test/resources/securityconfig.ini", TEST_KEY, false);
+
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                onException(UnknownAccountException.class, 
IncorrectCredentialsException.class,
+                        LockedAccountException.class, 
AuthenticationException.class).to("mock:authenticationException");
+
+                
from("direct:secureEndpoint").policy(securityPolicy).to("mock:success");
+            }
+        };
+    }
+
+    private static class TestShiroSecurityTokenInjector extends 
ShiroSecurityTokenInjector {
+
+        TestShiroSecurityTokenInjector(ShiroSecurityToken shiroSecurityToken, 
byte[] bytes) {
+            super(shiroSecurityToken, bytes);
+        }
+
+        @Override
+        public void process(Exchange exchange) {
+            
exchange.getIn().setHeader(ShiroSecurityConstants.SHIRO_SECURITY_TOKEN, 
encrypt());
+            exchange.getIn().setBody("Beatle Mania");
+        }
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 30e8dfb1d3ec..74b7f414031b 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -703,6 +703,22 @@ inherit values set on `defaultInstance`, and 
`defaultInstance` itself remains un
 Routes that compared unmarshalled bodies by identity, or that mutated one body 
expecting the change to
 be visible on another, must be updated.
 
+=== camel-shiro
+
+`ShiroSecurityProcessor` used to skip the Shiro `login()` call — and therefore 
the credential check —
+when the thread-bound subject was already authenticated for the same username 
as the incoming
+`ShiroSecurityToken`. The check conflated "same principal name" with "same 
credentials", so once a user
+had authenticated on a worker thread, a later exchange presenting that 
username with any password was
+accepted for as long as the subject stayed bound.
+
+The default `alwaysReauthenticate=true` masked this, because the processor 
calls `logout()` after each
+exchange. With `alwaysReauthenticate=false` — a documented option, which also 
sets `rememberMe(true)` to
+keep subjects long-lived — the skip was reachable.
+
+`login()` is now called for every exchange with the credentials that exchange 
presented. Deployments
+using `alwaysReauthenticate=false` will see one realm lookup per exchange 
where previously matching
+usernames reused the bound subject; correctness aside, that is the same cost 
the default already pays.
+
 === camel-oauth
 
 The authorization code flow now sends a `state` parameter and requires it back 
on the callback.

Reply via email to