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

voidmatcha pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git


The following commit(s) were added to refs/heads/master by this push:
     new 614baac846 [ZEPPELIN-6731] Fix LdapRealm login failure caused by 
unsupported SHA-1 hashing after Shiro 2.0.6 upgrade
614baac846 is described below

commit 614baac84668a7c880161b8df8f1d575be4d8aa2
Author: JangAyeon <[email protected]>
AuthorDate: Sat Oct 24 10:59:52 2026 +0900

    [ZEPPELIN-6731] Fix LdapRealm login failure caused by unsupported SHA-1 
hashing after Shiro 2.0.6 upgrade
    
    ### What is this PR for?
    Since the Shiro 2.0.6 upgrade (ZEPPELIN-6528), logging in through 
`LdapRealm` fails with HTTP 403 even with the correct password, because 
`createAuthenticationInfo()` hashes the credentials with `SHA-1`, which Shiro 
2's `SimpleHashProvider` no longer supports (only SHA-256/384/512).
    
    Switching the algorithm alone is not enough. In Shiro 2, 
`DefaultHashService` defaults to 50,000 iterations with a random salt, while 
`HashedCredentialsMatcher` defaults to 1 iteration. With only the algorithm 
changed, the exception disappears but the hashes never match, so login still 
fails silently with 403.
    
    This PR:
    - Switches `HASHING_ALGORITHM` from `SHA-1` to `SHA-256`.
    - Introduces `HASH_ITERATIONS` (Shiro's 
`SimpleHashProvider.Parameters.DEFAULT_ITERATIONS`) and uses it on both sides: 
the hash request in `createAuthenticationInfo()` and the 
`HashedCredentialsMatcher`.
    - Adds unit tests covering the `createAuthenticationInfo()` → credentials 
matcher path, which previously had no test coverage.
    
    
    ### What type of PR is it?
    Bug Fix
    
    ### Todos
    * [x] Switch `LdapRealm` hashing algorithm from `SHA-1` to `SHA-256`
    * [x] Align hash iterations between `DefaultHashService` and 
`HashedCredentialsMatcher`
    * [x] Add unit tests for the `createAuthenticationInfo()` → credentials 
matcher path
    * [x] Verify login manually against a real LDAP server (389 Directory 
Server)
    
    ### What is the Jira issue?
    [ZEPPELIN-6731](https://issues.apache.org/jira/browse/ZEPPELIN-6731)
    
    ### How should this be tested?
    Unit tests:
    - `LdapRealmTest#testCreateAuthenticationInfoMatchesSubmittedCredentials`
    - `LdapRealmTest#testCreateAuthenticationInfoRejectsDifferentCredentials`
    
    | Code state | Result |
    |---|---|
    | Before fix (`SHA-1`) | Both tests error: `UnsupportedOperationException: 
Cannot create a hash with the given algorithm: SHA-1` |
    | Algorithm changed to `SHA-256` only | Match test fails: `expected: <true> 
but was: <false>` (iteration mismatch) |
    | This PR | All tests in `LdapRealmTest`, `RoleMappingLdapRealmTest`, 
`LdapRealmFilterInjectionTest`, `LdapRealmDnInjectionTest` pass |
    
    Manual test, following the reproduction steps in the Jira issue (389 
Directory Server in Docker, `LdapRealm` in `shiro.ini`):
    
    | Password | Before | After |
    |---|---|---|
    | `alice123` (correct) | HTTP 403 + SHA-1 exception in server log | HTTP 
200 |
    | `wrong-pw` | HTTP 403 | HTTP 403 |
    
    
    ### Questions:
    * Does the license files need to update? No
    * Is there breaking changes for older versions? No
    * Does this needs documentation? No
    
    
    Closes #5585 from JangAyeon/ZEPPELIN-6731.
    
    Signed-off-by: YONGJAE LEE <[email protected]>
---
 .../java/org/apache/zeppelin/realm/LdapRealm.java  | 11 +++++++--
 .../org/apache/zeppelin/realm/LdapRealmTest.java   | 27 ++++++++++++++++++++++
 2 files changed, 36 insertions(+), 2 deletions(-)

diff --git 
a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/LdapRealm.java 
b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/LdapRealm.java
index e4c0df1496..46edfbd459 100644
--- a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/LdapRealm.java
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/LdapRealm.java
@@ -60,6 +60,7 @@ import org.apache.shiro.authz.SimpleAuthorizationInfo;
 import org.apache.shiro.crypto.hash.DefaultHashService;
 import org.apache.shiro.crypto.hash.Hash;
 import org.apache.shiro.crypto.hash.HashRequest;
+import org.apache.shiro.crypto.hash.SimpleHashProvider;
 import org.apache.shiro.crypto.hash.HashService;
 import org.apache.shiro.realm.ldap.DefaultLdapRealm;
 import org.apache.shiro.realm.ldap.JndiLdapContextFactory;
@@ -145,7 +146,10 @@ public class LdapRealm extends DefaultLdapRealm {
 
   private static final String DEFAULT_PRINCIPAL_REGEX = "(.*)";
   private static final String MEMBER_SUBSTITUTION_TOKEN = "{0}";
-  private static final String HASHING_ALGORITHM = "SHA-1";
+  private static final String HASHING_ALGORITHM = "SHA-256";
+  // The hash service and the credentials matcher must use the same iteration 
count,
+  // otherwise the cached credentials never match the submitted ones 
(ZEPPELIN-6731).
+  private static final int HASH_ITERATIONS = 
SimpleHashProvider.Parameters.DEFAULT_ITERATIONS;
   private static final Logger LOGGER = 
LoggerFactory.getLogger(LdapRealm.class);
 
   static {
@@ -204,6 +208,7 @@ public class LdapRealm extends DefaultLdapRealm {
 
   public LdapRealm() {
     HashedCredentialsMatcher credentialsMatcher = new 
HashedCredentialsMatcher(HASHING_ALGORITHM);
+    credentialsMatcher.setHashIterations(HASH_ITERATIONS);
     setCredentialsMatcher(credentialsMatcher);
   }
 
@@ -1185,7 +1190,9 @@ public class LdapRealm extends DefaultLdapRealm {
     HashRequest.Builder builder = new HashRequest.Builder();
     Hash credentialsHash = hashService
         .computeHash(builder.setSource(token.getCredentials())
-            .setAlgorithmName(HASHING_ALGORITHM).build());
+            .setAlgorithmName(HASHING_ALGORITHM)
+            .addParameter(SimpleHashProvider.Parameters.PARAMETER_ITERATIONS, 
HASH_ITERATIONS)
+            .build());
     return new SimpleAuthenticationInfo(token.getPrincipal(),
         credentialsHash.toHex(), credentialsHash.getSalt(),
         getName());
diff --git 
a/zeppelin-server/src/test/java/org/apache/zeppelin/realm/LdapRealmTest.java 
b/zeppelin-server/src/test/java/org/apache/zeppelin/realm/LdapRealmTest.java
index 415d80e6ec..f2df2322e3 100644
--- a/zeppelin-server/src/test/java/org/apache/zeppelin/realm/LdapRealmTest.java
+++ b/zeppelin-server/src/test/java/org/apache/zeppelin/realm/LdapRealmTest.java
@@ -19,7 +19,9 @@
 package org.apache.zeppelin.realm;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
@@ -28,6 +30,8 @@ import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import org.apache.shiro.authc.AuthenticationInfo;
+import org.apache.shiro.authc.UsernamePasswordToken;
 import org.apache.shiro.realm.ldap.LdapContextFactory;
 import org.apache.shiro.session.Session;
 import org.apache.shiro.subject.SimplePrincipalCollection;
@@ -48,6 +52,29 @@ import javax.naming.directory.SearchResult;
 import javax.naming.ldap.LdapContext;
 
 class LdapRealmTest {
+  @Test
+  void testCreateAuthenticationInfoMatchesSubmittedCredentials() throws 
NamingException {
+    // ZEPPELIN-6731: after a successful LDAP bind, the cached credentials 
must be
+    // hashable with the installed Shiro hash providers and must match the 
same password.
+    LdapRealm realm = new LdapRealm();
+    UsernamePasswordToken token = new UsernamePasswordToken("alice", 
"alice123");
+
+    AuthenticationInfo info = realm.createAuthenticationInfo(token, "alice", 
"alice123", null);
+
+    assertTrue(realm.getCredentialsMatcher().doCredentialsMatch(token, info));
+  }
+
+  @Test
+  void testCreateAuthenticationInfoRejectsDifferentCredentials() throws 
NamingException {
+    LdapRealm realm = new LdapRealm();
+    UsernamePasswordToken token = new UsernamePasswordToken("alice", 
"alice123");
+    AuthenticationInfo info = realm.createAuthenticationInfo(token, "alice", 
"alice123", null);
+
+    UsernamePasswordToken wrongToken = new UsernamePasswordToken("alice", 
"wrong-pw");
+
+    assertFalse(realm.getCredentialsMatcher().doCredentialsMatch(wrongToken, 
info));
+  }
+
   @Test
   void testGetUserDn() {
     LdapRealm realm = new LdapRealm();

Reply via email to