This is an automated email from the ASF dual-hosted git repository.
lprimak pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/shiro.git
The following commit(s) were added to refs/heads/main by this push:
new e6f534860 [#2841] Fix Active Directory DN authentication (#2854)
e6f534860 is described below
commit e6f534860072375e0b479b6eab0b1a076dc6798b
Author: Abhirama <[email protected]>
AuthorDate: Sun Aug 16 23:17:14 2026 +0530
[#2841] Fix Active Directory DN authentication (#2854)
---
.../activedirectory/ActiveDirectoryRealm.java | 44 +++++++++-
...ractRememberMeManagerObjectInputFilterTest.java | 23 +++---
.../activedirectory/ActiveDirectoryRealmTest.java | 96 +++++++++++++++++++---
3 files changed, 139 insertions(+), 24 deletions(-)
diff --git
a/core/src/main/java/org/apache/shiro/realm/activedirectory/ActiveDirectoryRealm.java
b/core/src/main/java/org/apache/shiro/realm/activedirectory/ActiveDirectoryRealm.java
index 34202b140..85d465dbd 100644
---
a/core/src/main/java/org/apache/shiro/realm/activedirectory/ActiveDirectoryRealm.java
+++
b/core/src/main/java/org/apache/shiro/realm/activedirectory/ActiveDirectoryRealm.java
@@ -24,6 +24,7 @@ import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
+import org.apache.shiro.lang.util.StringUtils;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.realm.ldap.AbstractLdapRealm;
import org.apache.shiro.realm.ldap.LdapContextFactory;
@@ -39,6 +40,7 @@ import javax.naming.directory.Attributes;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.LdapContext;
+import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import java.util.Collection;
import java.util.HashSet;
@@ -109,7 +111,7 @@ public class ActiveDirectoryRealm extends AbstractLdapRealm
{
// Binds using the username and password provided by the user.
LdapContext ctx = null;
try {
- ctx =
ldapContextFactory.getLdapContext(getUsernameWithSuffix(upToken.getUsername()),
+ ctx =
ldapContextFactory.getLdapContext(getUsernameWithSuffixOrFullDN(upToken.getUsername()),
String.valueOf(upToken.getPassword()));
} finally {
LdapUtils.closeContext(ctx);
@@ -168,7 +170,7 @@ public class ActiveDirectoryRealm extends AbstractLdapRealm
{
SearchControls searchControls = new SearchControls();
searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
- String userPrincipalName = getUsernameWithSuffix(username);
+ String userPrincipalName = getUsernameWithSuffixOrFullDN(username);
Object[] searchArguments = new Object[] {userPrincipalName};
@@ -234,6 +236,43 @@ public class ActiveDirectoryRealm extends
AbstractLdapRealm {
return roleNames;
}
+ /**
+ * Returns the username to use for authentication.
+ * If {@link #principalSuffix} is configured, the sanitized username with
appended suffix will be returned.
+ * If {@link #principalSuffix} is not configured, the method will check if
the username is a valid LDAP DN.
+ * If it is a valid LDAP DN, the username will be returned as-is,
+ * otherwise the sanitized username with appended suffix will be returned.
+ *
+ * @param username input to check and sanitize
+ * @return the sanitized username with optional suffix to use for
authentication
+ */
+ protected String getUsernameWithSuffixOrFullDN(String username) {
+ if (!StringUtils.hasText(principalSuffix)) {
+ try {
+ LdapName ldapName = new LdapName(username);
+ // Full LDAP DN needs to have more than one RDN, so we can
return the username as-is
+ if (ldapName.size() > 1) {
+ return username;
+ }
+ } catch (javax.naming.InvalidNameException e) {
+ // Not a valid LDAP DN, so treat it as a regular username.
+ }
+ }
+
+ return getUsernameWithSuffix(username);
+ }
+
+ /**
+ * Returns the sanitized username with appended suffix if {@link
#principalSuffix} is configured
+ * and the username does not already end with it.
+ * If {@link #principalSuffix} is not configured, the sanitized username
will be returned
+ * <p>
+ * NOTE: {@link #getUsernameWithSuffixOrFullDN(String)} should be used
instead of this method
+ * to handle full LDAP DNs correctly.
+ *
+ * @param username input to sanitize and append suffix
+ * @return the sanitized username with optional suffix
+ */
protected String getUsernameWithSuffix(String username) {
String sanitizedUsername = Rdn.escapeValue(username);
if (principalSuffix != null
@@ -242,5 +281,4 @@ public class ActiveDirectoryRealm extends AbstractLdapRealm
{
}
return sanitizedUsername;
}
-
}
diff --git
a/core/src/test/java/org/apache/shiro/mgt/AbstractRememberMeManagerObjectInputFilterTest.java
b/core/src/test/java/org/apache/shiro/mgt/AbstractRememberMeManagerObjectInputFilterTest.java
index f286487e3..5b819fa15 100644
---
a/core/src/test/java/org/apache/shiro/mgt/AbstractRememberMeManagerObjectInputFilterTest.java
+++
b/core/src/test/java/org/apache/shiro/mgt/AbstractRememberMeManagerObjectInputFilterTest.java
@@ -19,18 +19,21 @@
package org.apache.shiro.mgt;
import org.apache.shiro.lang.io.Serializer;
+import org.apache.shiro.subject.ImmutablePrincipalCollection;
import org.apache.shiro.subject.PrincipalCollection;
-import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.SubjectContext;
import org.apache.shiro.subject.support.DefaultSubjectContext;
import org.junit.jupiter.api.Test;
+import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InvalidClassException;
import java.io.ObjectInputFilter;
+import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
+import java.io.Serial;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@@ -51,7 +54,7 @@ class AbstractRememberMeManagerObjectInputFilterTest {
@Test
void testLegitimatePrincipalsRoundTripUnderDefaultFilter() {
InMemoryRememberMeManager rmm = new InMemoryRememberMeManager();
- PrincipalCollection principals = new
SimplePrincipalCollection("joecool", "myRealm");
+ PrincipalCollection principals =
ImmutablePrincipalCollection.ofSinglePrincipal("joecool", "myRealm");
rmm.rememberIdentity(null, principals);
PrincipalCollection remembered = rmm.getRememberedPrincipals(new
DefaultSubjectContext());
@@ -95,18 +98,18 @@ class AbstractRememberMeManagerObjectInputFilterTest {
@Test
void testCustomStricterAllowListFilterCanBeConfigured() {
// Documented override path (see
AbstractRememberMeManager#getSerializer javadoc): replace the default
- // serializer's filter with a strict class allow-list. Only
SimplePrincipalCollection,
+ // serializer's filter with a strict class allow-list. Only
ImmutablePrincipalCollection,
// AbstractRememberMeManager.RememberedIdentity, and JDK
collection/primitive/java.time plumbing are let
// through. Note: java.time types (e.g. Instant) don't serialize
themselves directly - they writeReplace()
// to an internal java.time serialization proxy class, which is what
actually appears in the stream.
InMemoryRememberMeManager rmm = new InMemoryRememberMeManager();
rmm.getSerializer()
.setObjectInputFilter(ObjectInputFilter.Config.createFilter(
- "org.apache.shiro.subject.SimplePrincipalCollection;"
+
"org.apache.shiro.subject.ImmutablePrincipalCollection;"
+
"org.apache.shiro.mgt.AbstractRememberMeManager$RememberedIdentity;"
+ "java.time.*;java.util.*;java.lang.*;!*"));
- PrincipalCollection principals = new
SimplePrincipalCollection("joecool", "myRealm");
+ PrincipalCollection principals =
ImmutablePrincipalCollection.ofSinglePrincipal("joecool", "myRealm");
rmm.rememberIdentity(null, principals);
PrincipalCollection remembered = rmm.getRememberedPrincipals(new
DefaultSubjectContext());
assertThat(remembered.getPrimaryPrincipal()).isEqualTo("joecool");
@@ -126,17 +129,16 @@ class AbstractRememberMeManagerObjectInputFilterTest {
// A caller-supplied Serializer implementation (not a
DefaultSerializer) must keep working exactly as
// before this feature existed - AbstractRememberMeManager only
touches the filter on its own default
// DefaultSerializer instance, never on a replaced Serializer.
- InMemoryRememberMeManager rmm = new InMemoryRememberMeManager();
- rmm.setSerializer(new
Serializer<AbstractRememberMeManager.RememberedIdentity>() {
+ var rmm = new InMemoryRememberMeManager();
+ rmm.setSerializer(new Serializer<>() {
@Override
public byte[]
serialize(AbstractRememberMeManager.RememberedIdentity o) {
return plainJdkSerialize(o);
}
@Override
- @SuppressWarnings("unchecked")
public AbstractRememberMeManager.RememberedIdentity
deserialize(byte[] serialized) {
- try (var ois = new java.io.ObjectInputStream(new
java.io.ByteArrayInputStream(serialized))) {
+ try (var ois = new ObjectInputStream(new
ByteArrayInputStream(serialized))) {
return (AbstractRememberMeManager.RememberedIdentity)
ois.readObject();
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
@@ -144,7 +146,7 @@ class AbstractRememberMeManagerObjectInputFilterTest {
}
});
- PrincipalCollection principals = new
SimplePrincipalCollection("joecool", "myRealm");
+ PrincipalCollection principals =
ImmutablePrincipalCollection.ofSinglePrincipal("joecool", "myRealm");
rmm.rememberIdentity(null, principals);
PrincipalCollection remembered = rmm.getRememberedPrincipals(new
DefaultSubjectContext());
@@ -164,6 +166,7 @@ class AbstractRememberMeManagerObjectInputFilterTest {
}
public static class NotAllowlisted implements Serializable {
+ @Serial
private static final long serialVersionUID = 1L;
}
diff --git
a/core/src/test/java/org/apache/shiro/realm/activedirectory/ActiveDirectoryRealmTest.java
b/core/src/test/java/org/apache/shiro/realm/activedirectory/ActiveDirectoryRealmTest.java
index 953a5724d..6b25db52e 100644
---
a/core/src/test/java/org/apache/shiro/realm/activedirectory/ActiveDirectoryRealmTest.java
+++
b/core/src/test/java/org/apache/shiro/realm/activedirectory/ActiveDirectoryRealmTest.java
@@ -55,6 +55,7 @@ import javax.naming.NamingException;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.LdapContext;
+import javax.naming.ldap.Rdn;
import java.util.HashSet;
import java.util.Set;
@@ -65,8 +66,11 @@ import static org.easymock.EasyMock.anyObject;
import static org.easymock.EasyMock.anyString;
import static org.easymock.EasyMock.capture;
import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.createNiceMock;
+import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
/**
@@ -149,6 +153,82 @@ public class ActiveDirectoryRealmTest {
SecurityUtils.setSecurityManager(null);
}
+ @Test
+ void usernameForAuthenticationWithDnAndSuffix() {
+ var activeDirectoryRealm = new ActiveDirectoryRealm();
+ activeDirectoryRealm.setPrincipalSuffix("@example.com");
+
+ String dn = "CN=my_name,OU=Development,OU=Special
Accounts,DC=mycompany,DC=com";
+
+ assertThat(activeDirectoryRealm.getUsernameWithSuffixOrFullDN(dn))
+ .isEqualTo(Rdn.escapeValue(dn) + "@example.com");
+ }
+
+ @Test
+ void usernameForAuthenticationWithDn() {
+ var activeDirectoryRealm = new ActiveDirectoryRealm();
+
+ String dn = "CN=my_name,OU=Development,OU=Special
Accounts,DC=mycompany,DC=com";
+
+ assertThat(activeDirectoryRealm.getUsernameWithSuffixOrFullDN(dn))
+ .isEqualTo(dn);
+ }
+
+ @Test
+ void testUsernameForAuthenticationWithUsername() {
+ var activeDirectoryRealm = new ActiveDirectoryRealm();
+
+
assertThat(activeDirectoryRealm.getUsernameWithSuffixOrFullDN("test,user"))
+ .isEqualTo("test\\,user");
+ }
+
+ @Test
+ void authenticationUsesDnWithoutEscaping() throws Exception {
+ var activeDirectoryRealm = new ActiveDirectoryRealm();
+ LdapContextFactory factory = createMock(LdapContextFactory.class);
+ LdapContext ldapContext = createNiceMock(LdapContext.class);
+
+ String dn = "CN=my_name,OU=Development,OU=Special
Accounts,DC=mycompany,DC=com";
+
+ expect(factory.getLdapContext(eq(dn),
anyObject())).andReturn(ldapContext);
+ replay(factory);
+
+ UsernamePasswordToken token = new UsernamePasswordToken(dn, PASSWORD);
+
+ activeDirectoryRealm.queryForAuthenticationInfo(token, factory);
+
+ verify(factory);
+ }
+
+ @Test
+ void authorizationUsesDnWithoutEscaping() throws Exception {
+ var activeDirectoryRealm = new ActiveDirectoryRealm();
+
+ LdapContext ldapContext = createNiceMock(LdapContext.class);
+ NamingEnumeration<SearchResult> results =
createNiceMock(NamingEnumeration.class);
+
+ String dn = "CN=my_name,OU=Development,OU=Special
Accounts,DC=mycompany,DC=com";
+
+ Capture<Object[]> captureArgs = Capture.newInstance(CaptureType.ALL);
+
+ expect(ldapContext.search(
+ anyString(),
+ anyString(),
+ capture(captureArgs),
+ anyObject(SearchControls.class)))
+ .andReturn(results);
+
+ replay(ldapContext);
+
+ activeDirectoryRealm.getRoleNamesForUser(dn, ldapContext);
+
+ Object[] searchArguments = captureArgs.getValue();
+
+ assertThat(searchArguments[0]).isEqualTo(dn);
+
+ verify(ldapContext);
+ }
+
public void assertExistingUserSuffix(String username, String
expectedPrincipalName) throws Exception {
LdapContext ldapContext = createMock(LdapContext.class);
@@ -158,9 +238,8 @@ public class ActiveDirectoryRealmTest {
.andReturn(results);
replay(ldapContext);
- ActiveDirectoryRealm activeDirectoryRealm = new ActiveDirectoryRealm()
{{
- this.principalSuffix = "@ExAmple.COM";
- }};
+ var activeDirectoryRealm = new ActiveDirectoryRealm();
+ activeDirectoryRealm.setPrincipalSuffix("@ExAmple.COM");
SecurityManager securityManager = new
DefaultSecurityManager(activeDirectoryRealm);
Subject subject = new Subject.Builder(securityManager).buildSubject();
@@ -203,10 +282,6 @@ public class ActiveDirectoryRealmTest {
setCredentialsMatcher(credentialsMatcher);
}
- public void setPrincipalSuffix(String principalSuffix) {
- this.principalSuffix = principalSuffix;
- }
-
protected AuthenticationInfo
doGetAuthenticationInfo(AuthenticationToken token) throws
AuthenticationException {
SimpleAccount account = (SimpleAccount)
super.doGetAuthenticationInfo(token);
@@ -222,17 +297,16 @@ public class ActiveDirectoryRealmTest {
}
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection
principals) {
- Set<String> roles = new HashSet<String>();
+ Set<String> roles = new HashSet<>();
roles.add(ROLE);
return new SimpleAuthorizationInfo(roles);
}
// override ldap query because i don't care about testing that piece
in this case
- protected AuthenticationInfo
queryForAuthenticationInfo(AuthenticationToken token, LdapContextFactory
ldapContextFactory)
- throws NamingException {
+ protected AuthenticationInfo
queryForAuthenticationInfo(AuthenticationToken token,
+
LdapContextFactory ldapContextFactory) {
return new SimpleAccount(token.getPrincipal(),
token.getCredentials(), getName());
}
-
}
}