瀏覽代碼

Polish LDAP Module

Issue gh-3834
Josh Cummings 1 年之前
父節點
當前提交
78d2be9bd5
共有 25 個文件被更改,包括 97 次插入321 次删除
  1. 5 16
      ldap/src/main/java/org/springframework/security/ldap/DefaultSpringSecurityContextSource.java
  2. 2 126
      ldap/src/main/java/org/springframework/security/ldap/LdapEncoder.java
  3. 1 1
      ldap/src/main/java/org/springframework/security/ldap/LdapUtils.java
  4. 14 14
      ldap/src/main/java/org/springframework/security/ldap/SpringSecurityLdapTemplate.java
  5. 5 3
      ldap/src/main/java/org/springframework/security/ldap/authentication/AbstractLdapAuthenticationProvider.java
  6. 7 4
      ldap/src/main/java/org/springframework/security/ldap/authentication/AbstractLdapAuthenticator.java
  7. 1 1
      ldap/src/main/java/org/springframework/security/ldap/authentication/LdapAuthenticator.java
  8. 4 95
      ldap/src/main/java/org/springframework/security/ldap/authentication/LdapEncoder.java
  9. 2 3
      ldap/src/main/java/org/springframework/security/ldap/authentication/SpringSecurityAuthenticationSource.java
  10. 1 2
      ldap/src/main/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryAuthenticationException.java
  11. 1 2
      ldap/src/main/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProvider.java
  12. 2 2
      ldap/src/main/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixin.java
  13. 2 2
      ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixin.java
  14. 2 1
      ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapJackson2Module.java
  15. 2 2
      ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixin.java
  16. 2 2
      ldap/src/main/java/org/springframework/security/ldap/jackson2/PersonMixin.java
  17. 2 2
      ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyAwareContextSource.java
  18. 1 1
      ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyResponseControl.java
  19. 3 3
      ldap/src/main/java/org/springframework/security/ldap/search/FilterBasedLdapUserSearch.java
  20. 5 4
      ldap/src/main/java/org/springframework/security/ldap/server/UnboundIdContainer.java
  21. 2 12
      ldap/src/main/java/org/springframework/security/ldap/userdetails/DefaultLdapAuthoritiesPopulator.java
  22. 11 11
      ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapAuthority.java
  23. 16 8
      ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsManager.java
  24. 3 3
      ldap/src/main/java/org/springframework/security/ldap/userdetails/NestedLdapAuthoritiesPopulator.java
  25. 1 1
      ldap/src/main/java/org/springframework/security/ldap/userdetails/UserDetailsContextMapper.java

+ 5 - 16
ldap/src/main/java/org/springframework/security/ldap/DefaultSpringSecurityContextSource.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2024 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,7 +16,6 @@
 
 package org.springframework.security.ldap;
 
-import java.io.UnsupportedEncodingException;
 import java.net.URLDecoder;
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
@@ -84,7 +83,7 @@ public class DefaultSpringSecurityContextSource extends LdapContextSource {
 		setAuthenticationStrategy(new SimpleDirContextAuthenticationStrategy() {
 
 			@Override
-			@SuppressWarnings("rawtypes")
+			@SuppressWarnings("unchecked")
 			public void setupEnvironment(Hashtable env, String dn, String password) {
 				super.setupEnvironment(env, dn, password);
 				// Remove the pooling flag unless authenticating as the 'manager' user.
@@ -145,7 +144,7 @@ public class DefaultSpringSecurityContextSource extends LdapContextSource {
 		StringBuilder providerUrl = new StringBuilder();
 		for (String serverUrl : urls) {
 			String trimmedUrl = serverUrl.trim();
-			if ("".equals(trimmedUrl)) {
+			if (trimmedUrl.isEmpty()) {
 				continue;
 			}
 			providerUrl.append(trimmedUrl);
@@ -160,21 +159,11 @@ public class DefaultSpringSecurityContextSource extends LdapContextSource {
 	}
 
 	private static String encodeUrl(String url) {
-		try {
-			return URLEncoder.encode(url, StandardCharsets.UTF_8.toString());
-		}
-		catch (UnsupportedEncodingException ex) {
-			throw new IllegalStateException(ex);
-		}
+		return URLEncoder.encode(url, StandardCharsets.UTF_8);
 	}
 
 	private String decodeUrl(String url) {
-		try {
-			return URLDecoder.decode(url, StandardCharsets.UTF_8.toString());
-		}
-		catch (UnsupportedEncodingException ex) {
-			throw new IllegalStateException(ex);
-		}
+		return URLDecoder.decode(url, StandardCharsets.UTF_8);
 	}
 
 }

+ 2 - 126
ldap/src/main/java/org/springframework/security/ldap/LdapEncoder.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2005-2010 the original author or authors.
+ * Copyright 2005-2024 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,8 +16,6 @@
 
 package org.springframework.security.ldap;
 
-import org.springframework.ldap.BadLdapGrammarException;
-
 /**
  * Helper class to encode and decode ldap names and values.
  *
@@ -31,26 +29,7 @@ import org.springframework.ldap.BadLdapGrammarException;
  */
 final class LdapEncoder {
 
-	private static final int HEX = 16;
-
-	private static String[] NAME_ESCAPE_TABLE = new String[96];
-	static {
-		// all below 0x20 (control chars)
-		for (char c = 0; c < ' '; c++) {
-			NAME_ESCAPE_TABLE[c] = "\\" + toTwoCharHex(c);
-		}
-		NAME_ESCAPE_TABLE['#'] = "\\#";
-		NAME_ESCAPE_TABLE[','] = "\\,";
-		NAME_ESCAPE_TABLE[';'] = "\\;";
-		NAME_ESCAPE_TABLE['='] = "\\=";
-		NAME_ESCAPE_TABLE['+'] = "\\+";
-		NAME_ESCAPE_TABLE['<'] = "\\<";
-		NAME_ESCAPE_TABLE['>'] = "\\>";
-		NAME_ESCAPE_TABLE['\"'] = "\\\"";
-		NAME_ESCAPE_TABLE['\\'] = "\\\\";
-	}
-
-	private static String[] FILTER_ESCAPE_TABLE = new String['\\' + 1];
+	private static final String[] FILTER_ESCAPE_TABLE = new String['\\' + 1];
 
 	static {
 		// fill with char itself
@@ -71,11 +50,6 @@ final class LdapEncoder {
 	private LdapEncoder() {
 	}
 
-	protected static String toTwoCharHex(char c) {
-		String raw = Integer.toHexString(c).toUpperCase();
-		return (raw.length() > 1) ? raw : "0" + raw;
-	}
-
 	/**
 	 * Escape a value for use in a filter.
 	 * @param value the value to escape.
@@ -94,102 +68,4 @@ final class LdapEncoder {
 		return encodedValue.toString();
 	}
 
-	/**
-	 * LDAP Encodes a value for use with a DN. Escapes for LDAP, not JNDI!
-	 *
-	 * <br/>
-	 * Escapes:<br/>
-	 * ' ' [space] - "\ " [if first or last] <br/>
-	 * '#' [hash] - "\#" <br/>
-	 * ',' [comma] - "\," <br/>
-	 * ';' [semicolon] - "\;" <br/>
-	 * '= [equals] - "\=" <br/>
-	 * '+' [plus] - "\+" <br/>
-	 * '&lt;' [less than] - "\&lt;" <br/>
-	 * '&gt;' [greater than] - "\&gt;" <br/>
-	 * '"' [double quote] - "\"" <br/>
-	 * '\' [backslash] - "\\" <br/>
-	 * @param value the value to escape.
-	 * @return The escaped value.
-	 */
-	static String nameEncode(String value) {
-		if (value == null) {
-			return null;
-		}
-		StringBuilder encodedValue = new StringBuilder(value.length() * 2);
-		int length = value.length();
-		int last = length - 1;
-		for (int i = 0; i < length; i++) {
-			char c = value.charAt(i);
-			// space first or last
-			if (c == ' ' && (i == 0 || i == last)) {
-				encodedValue.append("\\ ");
-				continue;
-			}
-			// check in table for escapes
-			if (c < NAME_ESCAPE_TABLE.length) {
-				String esc = NAME_ESCAPE_TABLE[c];
-				if (esc != null) {
-					encodedValue.append(esc);
-					continue;
-				}
-			}
-			// default: add the char
-			encodedValue.append(c);
-		}
-		return encodedValue.toString();
-	}
-
-	/**
-	 * Decodes a value. Converts escaped chars to ordinary chars.
-	 * @param value Trimmed value, so no leading an trailing blanks, except an escaped
-	 * space last.
-	 * @return The decoded value as a string.
-	 * @throws BadLdapGrammarException
-	 */
-	static String nameDecode(String value) throws BadLdapGrammarException {
-		if (value == null) {
-			return null;
-		}
-		StringBuilder decoded = new StringBuilder(value.length());
-		int i = 0;
-		while (i < value.length()) {
-			char currentChar = value.charAt(i);
-			if (currentChar == '\\') {
-				// Ending with a single backslash is not allowed
-				if (value.length() <= i + 1) {
-					throw new BadLdapGrammarException("Unexpected end of value " + "unterminated '\\'");
-				}
-				char nextChar = value.charAt(i + 1);
-				if (isNormalBackslashEscape(nextChar)) {
-					decoded.append(nextChar);
-					i += 2;
-				}
-				else {
-					if (value.length() <= i + 2) {
-						throw new BadLdapGrammarException(
-								"Unexpected end of value " + "expected special or hex, found '" + nextChar + "'");
-					}
-					// This should be a hex value
-					String hexString = "" + nextChar + value.charAt(i + 2);
-					decoded.append((char) Integer.parseInt(hexString, HEX));
-					i += 3;
-				}
-			}
-			else {
-				// This character wasn't escaped - just append it
-				decoded.append(currentChar);
-				i++;
-			}
-		}
-
-		return decoded.toString();
-
-	}
-
-	private static boolean isNormalBackslashEscape(char nextChar) {
-		return nextChar == ',' || nextChar == '=' || nextChar == '+' || nextChar == '<' || nextChar == '>'
-				|| nextChar == '#' || nextChar == ';' || nextChar == '\\' || nextChar == '\"' || nextChar == ' ';
-	}
-
 }

+ 1 - 1
ldap/src/main/java/org/springframework/security/ldap/LdapUtils.java

@@ -83,7 +83,7 @@ public final class LdapUtils {
 	 */
 	public static String getRelativeName(String fullDn, Context baseCtx) throws NamingException {
 		String baseDn = baseCtx.getNameInNamespace();
-		if (baseDn.length() == 0) {
+		if (baseDn.isEmpty()) {
 			return fullDn;
 		}
 		LdapName base = LdapNameBuilder.newInstance(baseDn).build();

+ 14 - 14
ldap/src/main/java/org/springframework/security/ldap/SpringSecurityLdapTemplate.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2013 the original author or authors.
+ * Copyright 2002-2024 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -18,7 +18,7 @@ package org.springframework.security.ldap;
 
 import java.text.MessageFormat;
 import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -40,7 +40,6 @@ import org.apache.commons.logging.LogFactory;
 
 import org.springframework.core.log.LogMessage;
 import org.springframework.dao.IncorrectResultSizeDataAccessException;
-import org.springframework.ldap.core.ContextExecutor;
 import org.springframework.ldap.core.ContextMapper;
 import org.springframework.ldap.core.ContextSource;
 import org.springframework.ldap.core.DirContextAdapter;
@@ -98,7 +97,7 @@ public class SpringSecurityLdapTemplate extends LdapTemplate {
 			searchControls.setSearchScope(SearchControls.OBJECT_SCOPE);
 			Object[] params = new Object[] { value };
 			NamingEnumeration<SearchResult> results = ctx.search(dn, comparisonFilter, params, searchControls);
-			Boolean match = results.hasMore();
+			boolean match = results.hasMore();
 			LdapUtils.closeEnumeration(results);
 			return match;
 		});
@@ -112,7 +111,7 @@ public class SpringSecurityLdapTemplate extends LdapTemplate {
 	 * @return the object created by the mapper
 	 */
 	public DirContextOperations retrieveEntry(final String dn, final String[] attributesToRetrieve) {
-		return (DirContextOperations) executeReadOnly((ContextExecutor) (ctx) -> {
+		return executeReadOnly((ctx) -> {
 			Attributes attrs = ctx.getAttributes(dn, attributesToRetrieve);
 			return new DirContextAdapter(attrs, LdapNameBuilder.newInstance(dn).build(),
 					LdapNameBuilder.newInstance(ctx.getNameInNamespace()).build());
@@ -169,18 +168,19 @@ public class SpringSecurityLdapTemplate extends LdapTemplate {
 		String formattedFilter = MessageFormat.format(filter, encodedParams);
 		logger.trace(LogMessage.format("Using filter: %s", formattedFilter));
 		HashSet<Map<String, List<String>>> result = new HashSet<>();
-		ContextMapper roleMapper = (ctx) -> {
+		ContextMapper<?> roleMapper = (ctx) -> {
 			DirContextAdapter adapter = (DirContextAdapter) ctx;
 			Map<String, List<String>> record = new HashMap<>();
 			if (ObjectUtils.isEmpty(attributeNames)) {
 				try {
-					for (NamingEnumeration enumeration = adapter.getAttributes().getAll(); enumeration.hasMore();) {
-						Attribute attr = (Attribute) enumeration.next();
+					for (NamingEnumeration<? extends Attribute> enumeration = adapter.getAttributes()
+						.getAll(); enumeration.hasMore();) {
+						Attribute attr = enumeration.next();
 						extractStringAttributeValues(adapter, record, attr.getID());
 					}
 				}
 				catch (NamingException ex) {
-					org.springframework.ldap.support.LdapUtils.convertLdapException(ex);
+					throw org.springframework.ldap.support.LdapUtils.convertLdapException(ex);
 				}
 			}
 			else {
@@ -188,7 +188,7 @@ public class SpringSecurityLdapTemplate extends LdapTemplate {
 					extractStringAttributeValues(adapter, record, attributeName);
 				}
 			}
-			record.put(DN_KEY, Arrays.asList(getAdapterDN(adapter)));
+			record.put(DN_KEY, Collections.singletonList(getAdapterDN(adapter)));
 			result.add(record);
 			return null;
 		};
@@ -258,8 +258,7 @@ public class SpringSecurityLdapTemplate extends LdapTemplate {
 	 * search returns more than one result.
 	 */
 	public DirContextOperations searchForSingleEntry(String base, String filter, Object[] params) {
-		return (DirContextOperations) executeReadOnly((ContextExecutor) (ctx) -> searchForSingleEntryInternal(ctx,
-				this.searchControls, base, filter, params));
+		return executeReadOnly((ctx) -> searchForSingleEntryInternal(ctx, this.searchControls, base, filter, params));
 	}
 
 	/**
@@ -296,8 +295,9 @@ public class SpringSecurityLdapTemplate extends LdapTemplate {
 	/**
 	 * We need to make sure the search controls has the return object flag set to true, in
 	 * order for the search to return DirContextAdapter instances.
-	 * @param originalControls
-	 * @return
+	 * @param originalControls the {@link SearchControls} that might have the return
+	 * object flag set to true
+	 * @return a {@link SearchControls} that does have the return object flag set to true
 	 */
 	private static SearchControls buildControls(SearchControls originalControls) {
 		return new SearchControls(originalControls.getSearchScope(), originalControls.getCountLimit(),

+ 5 - 3
ldap/src/main/java/org/springframework/security/ldap/authentication/AbstractLdapAuthenticationProvider.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2024 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -24,6 +24,7 @@ import org.apache.commons.logging.LogFactory;
 import org.springframework.context.MessageSource;
 import org.springframework.context.MessageSourceAware;
 import org.springframework.context.support.MessageSourceAccessor;
+import org.springframework.lang.NonNull;
 import org.springframework.ldap.core.DirContextOperations;
 import org.springframework.security.authentication.AuthenticationProvider;
 import org.springframework.security.authentication.BadCredentialsException;
@@ -117,14 +118,15 @@ public abstract class AbstractLdapAuthenticationProvider implements Authenticati
 	 * obtained from the UserDetails object created by the configured
 	 * {@code UserDetailsContextMapper}. Often it will not be possible to read the
 	 * password from the directory, so defaults to true.
-	 * @param useAuthenticationRequestCredentials
+	 * @param useAuthenticationRequestCredentials whether to use the credentials in the
+	 * authentication request
 	 */
 	public void setUseAuthenticationRequestCredentials(boolean useAuthenticationRequestCredentials) {
 		this.useAuthenticationRequestCredentials = useAuthenticationRequestCredentials;
 	}
 
 	@Override
-	public void setMessageSource(MessageSource messageSource) {
+	public void setMessageSource(@NonNull MessageSource messageSource) {
 		this.messages = new MessageSourceAccessor(messageSource);
 	}
 

+ 7 - 4
ldap/src/main/java/org/springframework/security/ldap/authentication/AbstractLdapAuthenticator.java

@@ -25,6 +25,7 @@ import org.springframework.beans.factory.InitializingBean;
 import org.springframework.context.MessageSource;
 import org.springframework.context.MessageSourceAware;
 import org.springframework.context.support.MessageSourceAccessor;
+import org.springframework.lang.NonNull;
 import org.springframework.ldap.core.ContextSource;
 import org.springframework.security.core.SpringSecurityMessageSource;
 import org.springframework.security.ldap.search.LdapUserSearch;
@@ -37,6 +38,8 @@ import org.springframework.util.Assert;
  */
 public abstract class AbstractLdapAuthenticator implements LdapAuthenticator, InitializingBean, MessageSourceAware {
 
+	private final Object mutex = new Object();
+
 	private final ContextSource contextSource;
 
 	/**
@@ -59,7 +62,7 @@ public abstract class AbstractLdapAuthenticator implements LdapAuthenticator, In
 
 	/**
 	 * Create an initialized instance with the {@link ContextSource} provided.
-	 * @param contextSource
+	 * @param contextSource the {@link ContextSource} to use
 	 */
 	public AbstractLdapAuthenticator(ContextSource contextSource) {
 		Assert.notNull(contextSource, "contextSource must not be null.");
@@ -93,7 +96,7 @@ public abstract class AbstractLdapAuthenticator implements LdapAuthenticator, In
 		}
 		List<String> userDns = new ArrayList<>(this.userDnFormat.length);
 		String[] args = new String[] { LdapEncoder.nameEncode(username) };
-		synchronized (this.userDnFormat) {
+		synchronized (this.mutex) {
 			for (MessageFormat formatter : this.userDnFormat) {
 				userDns.add(formatter.format(args));
 			}
@@ -106,14 +109,14 @@ public abstract class AbstractLdapAuthenticator implements LdapAuthenticator, In
 	}
 
 	@Override
-	public void setMessageSource(MessageSource messageSource) {
+	public void setMessageSource(@NonNull MessageSource messageSource) {
 		Assert.notNull(messageSource, "Message source must not be null");
 		this.messages = new MessageSourceAccessor(messageSource);
 	}
 
 	/**
 	 * Sets the user attributes which will be retrieved from the directory.
-	 * @param userAttributes
+	 * @param userAttributes the set of user attributes to retrieve
 	 */
 	public void setUserAttributes(String[] userAttributes) {
 		Assert.notNull(userAttributes, "The userAttributes property cannot be set to null");

+ 1 - 1
ldap/src/main/java/org/springframework/security/ldap/authentication/LdapAuthenticator.java

@@ -33,7 +33,7 @@ public interface LdapAuthenticator {
 
 	/**
 	 * Authenticates as a user and obtains additional user information from the directory.
-	 * @param authentication
+	 * @param authentication the authentication request
 	 * @return the details of the successfully authenticated user.
 	 */
 	DirContextOperations authenticate(Authentication authentication);

+ 4 - 95
ldap/src/main/java/org/springframework/security/ldap/authentication/LdapEncoder.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2005-2010 the original author or authors.
+ * Copyright 2005-2024 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,8 +16,6 @@
 
 package org.springframework.security.ldap.authentication;
 
-import org.springframework.ldap.BadLdapGrammarException;
-
 /**
  * Helper class to encode and decode ldap names and values.
  *
@@ -31,9 +29,7 @@ import org.springframework.ldap.BadLdapGrammarException;
  */
 final class LdapEncoder {
 
-	private static final int HEX = 16;
-
-	private static String[] NAME_ESCAPE_TABLE = new String[96];
+	private static final String[] NAME_ESCAPE_TABLE = new String[96];
 	static {
 		// all below 0x20 (control chars)
 		for (char c = 0; c < ' '; c++) {
@@ -50,54 +46,19 @@ final class LdapEncoder {
 		NAME_ESCAPE_TABLE['\\'] = "\\\\";
 	}
 
-	private static String[] FILTER_ESCAPE_TABLE = new String['\\' + 1];
-
-	static {
-		// fill with char itself
-		for (char c = 0; c < FILTER_ESCAPE_TABLE.length; c++) {
-			FILTER_ESCAPE_TABLE[c] = String.valueOf(c);
-		}
-		// escapes (RFC2254)
-		FILTER_ESCAPE_TABLE['*'] = "\\2a";
-		FILTER_ESCAPE_TABLE['('] = "\\28";
-		FILTER_ESCAPE_TABLE[')'] = "\\29";
-		FILTER_ESCAPE_TABLE['\\'] = "\\5c";
-		FILTER_ESCAPE_TABLE[0] = "\\00";
-	}
-
 	/**
 	 * All static methods - not to be instantiated.
 	 */
 	private LdapEncoder() {
 	}
 
-	protected static String toTwoCharHex(char c) {
+	static String toTwoCharHex(char c) {
 		String raw = Integer.toHexString(c).toUpperCase();
 		return (raw.length() > 1) ? raw : "0" + raw;
 	}
 
 	/**
-	 * Escape a value for use in a filter.
-	 * @param value the value to escape.
-	 * @return a properly escaped representation of the supplied value.
-	 */
-	static String filterEncode(String value) {
-		if (value == null) {
-			return null;
-		}
-		StringBuilder encodedValue = new StringBuilder(value.length() * 2);
-		int length = value.length();
-		for (int i = 0; i < length; i++) {
-			char ch = value.charAt(i);
-			encodedValue.append((ch < FILTER_ESCAPE_TABLE.length) ? FILTER_ESCAPE_TABLE[ch] : ch);
-		}
-		return encodedValue.toString();
-	}
-
-	/**
-	 * LDAP Encodes a value for use with a DN. Escapes for LDAP, not JNDI!
-	 *
-	 * <br/>
+	 * LDAP Encodes a value for use with a DN. Escapes for LDAP, not JNDI! <br/>
 	 * Escapes:<br/>
 	 * ' ' [space] - "\ " [if first or last] <br/>
 	 * '#' [hash] - "\#" <br/>
@@ -140,56 +101,4 @@ final class LdapEncoder {
 		return encodedValue.toString();
 	}
 
-	/**
-	 * Decodes a value. Converts escaped chars to ordinary chars.
-	 * @param value Trimmed value, so no leading an trailing blanks, except an escaped
-	 * space last.
-	 * @return The decoded value as a string.
-	 * @throws BadLdapGrammarException
-	 */
-	static String nameDecode(String value) throws BadLdapGrammarException {
-		if (value == null) {
-			return null;
-		}
-		StringBuilder decoded = new StringBuilder(value.length());
-		int i = 0;
-		while (i < value.length()) {
-			char currentChar = value.charAt(i);
-			if (currentChar == '\\') {
-				// Ending with a single backslash is not allowed
-				if (value.length() <= i + 1) {
-					throw new BadLdapGrammarException("Unexpected end of value " + "unterminated '\\'");
-				}
-				char nextChar = value.charAt(i + 1);
-				if (isNormalBackslashEscape(nextChar)) {
-					decoded.append(nextChar);
-					i += 2;
-				}
-				else {
-					if (value.length() <= i + 2) {
-						throw new BadLdapGrammarException(
-								"Unexpected end of value " + "expected special or hex, found '" + nextChar + "'");
-					}
-					// This should be a hex value
-					String hexString = "" + nextChar + value.charAt(i + 2);
-					decoded.append((char) Integer.parseInt(hexString, HEX));
-					i += 3;
-				}
-			}
-			else {
-				// This character wasn't escaped - just append it
-				decoded.append(currentChar);
-				i++;
-			}
-		}
-
-		return decoded.toString();
-
-	}
-
-	private static boolean isNormalBackslashEscape(char nextChar) {
-		return nextChar == ',' || nextChar == '=' || nextChar == '+' || nextChar == '<' || nextChar == '>'
-				|| nextChar == '#' || nextChar == ';' || nextChar == '\\' || nextChar == '\"' || nextChar == ' ';
-	}
-
 }

+ 2 - 3
ldap/src/main/java/org/springframework/security/ldap/authentication/SpringSecurityAuthenticationSource.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2024 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -57,8 +57,7 @@ public class SpringSecurityAuthenticationSource implements AuthenticationSource
 			return "";
 		}
 		Object principal = authentication.getPrincipal();
-		if (principal instanceof LdapUserDetails) {
-			LdapUserDetails details = (LdapUserDetails) principal;
+		if (principal instanceof LdapUserDetails details) {
 			return details.getDn();
 		}
 		if (authentication instanceof AnonymousAuthenticationToken) {

+ 1 - 2
ldap/src/main/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryAuthenticationException.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2016 the original author or authors.
+ * Copyright 2002-2024 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -40,7 +40,6 @@ import org.springframework.security.core.AuthenticationException;
  *
  * @author Rob Winch
  */
-@SuppressWarnings("serial")
 public final class ActiveDirectoryAuthenticationException extends AuthenticationException {
 
 	private final String dataCode;

+ 1 - 2
ldap/src/main/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProvider.java

@@ -189,12 +189,11 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
 
 	private DirContext bindAsUser(String username, String password) {
 		// TODO. add DNS lookup based on domain
-		final String bindUrl = this.url;
 		Hashtable<String, Object> env = new Hashtable<>();
 		env.put(Context.SECURITY_AUTHENTICATION, "simple");
 		String bindPrincipal = createBindPrincipal(username);
 		env.put(Context.SECURITY_PRINCIPAL, bindPrincipal);
-		env.put(Context.PROVIDER_URL, bindUrl);
+		env.put(Context.PROVIDER_URL, this.url);
 		env.put(Context.SECURITY_CREDENTIALS, password);
 		env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
 		env.put(Context.OBJECT_FACTORIES, DefaultDirObjectFactory.class.getName());

+ 2 - 2
ldap/src/main/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixin.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2015-2021 the original author or authors.
+ * Copyright 2015-2024 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -30,7 +30,7 @@ import org.springframework.security.ldap.userdetails.InetOrgPerson;
  * @see LdapJackson2Module
  * @see SecurityJackson2Modules
  */
-@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
 @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
 		isGetterVisibility = JsonAutoDetect.Visibility.NONE)
 @JsonIgnoreProperties(ignoreUnknown = true)

+ 2 - 2
ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixin.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2015-2021 the original author or authors.
+ * Copyright 2015-2024 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -35,7 +35,7 @@ import org.springframework.security.ldap.userdetails.LdapAuthority;
  * @see LdapJackson2Module
  * @see SecurityJackson2Modules
  */
-@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
 @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE)
 @JsonIgnoreProperties(ignoreUnknown = true)
 abstract class LdapAuthorityMixin {

+ 2 - 1
ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapJackson2Module.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2015-2021 the original author or authors.
+ * Copyright 2015-2024 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@ import org.springframework.security.ldap.userdetails.Person;
  * {@link LdapAuthorityMixin}, {@link LdapUserDetailsImplMixin}, {@link PersonMixin},
  * {@link InetOrgPersonMixin}.
  *
+ * <p>
  * If not already enabled, default typing will be automatically enabled as type info is
  * required to properly serialize/deserialize objects. In order to use this module just
  * add it to your {@code ObjectMapper} configuration.

+ 2 - 2
ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixin.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2015-2021 the original author or authors.
+ * Copyright 2015-2024 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -30,7 +30,7 @@ import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl;
  * @see LdapJackson2Module
  * @see SecurityJackson2Modules
  */
-@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
 @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
 		isGetterVisibility = JsonAutoDetect.Visibility.NONE)
 @JsonIgnoreProperties(ignoreUnknown = true)

+ 2 - 2
ldap/src/main/java/org/springframework/security/ldap/jackson2/PersonMixin.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2015-2021 the original author or authors.
+ * Copyright 2015-2024 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -30,7 +30,7 @@ import org.springframework.security.ldap.userdetails.Person;
  * @see LdapJackson2Module
  * @see SecurityJackson2Modules
  */
-@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
 @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
 		isGetterVisibility = JsonAutoDetect.Visibility.NONE)
 @JsonIgnoreProperties(ignoreUnknown = true)

+ 2 - 2
ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyAwareContextSource.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2016 the original author or authors.
+ * Copyright 2002-2024 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -77,7 +77,7 @@ public class PasswordPolicyAwareContextSource extends DefaultSpringSecurityConte
 	@Override
 	@SuppressWarnings("unchecked")
 	protected Hashtable getAuthenticatedEnv(String principal, String credentials) {
-		Hashtable env = super.getAuthenticatedEnv(principal, credentials);
+		Hashtable<String, Object> env = super.getAuthenticatedEnv(principal, credentials);
 		env.put(LdapContext.CONTROL_FACTORIES, PasswordPolicyControlFactory.class.getName());
 		return env;
 	}

+ 1 - 1
ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyResponseControl.java

@@ -220,7 +220,7 @@ public class PasswordPolicyResponseControl extends PasswordPolicyControl {
 			}
 		}
 
-		class SpecificTagDecoder extends BERTagDecoder {
+		static class SpecificTagDecoder extends BERTagDecoder {
 
 			/** Allows us to remember which of the two options we're decoding */
 			private Boolean inChoice = null;

+ 3 - 3
ldap/src/main/java/org/springframework/security/ldap/search/FilterBasedLdapUserSearch.java

@@ -52,7 +52,7 @@ public class FilterBasedLdapUserSearch implements LdapUserSearch {
 	/**
 	 * Context name to search in, relative to the base of the configured ContextSource.
 	 */
-	private String searchBase = "";
+	private final String searchBase;
 
 	/**
 	 * The filter expression used in the user search. This is an LDAP search filter (as
@@ -78,9 +78,9 @@ public class FilterBasedLdapUserSearch implements LdapUserSearch {
 		this.contextSource = contextSource;
 		this.searchBase = searchBase;
 		setSearchSubtree(true);
-		if (searchBase.length() == 0) {
+		if (searchBase.isEmpty()) {
 			logger.info(LogMessage.format("Searches will be performed from the root %s since SearchBase not set",
-					contextSource.getBaseLdapPath()));
+					contextSource.getBaseLdapName()));
 		}
 	}
 

+ 5 - 4
ldap/src/main/java/org/springframework/security/ldap/server/UnboundIdContainer.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2024 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -33,6 +33,7 @@ import org.springframework.context.ApplicationContext;
 import org.springframework.context.ApplicationContextAware;
 import org.springframework.context.Lifecycle;
 import org.springframework.core.io.Resource;
+import org.springframework.lang.NonNull;
 import org.springframework.util.StringUtils;
 
 /**
@@ -43,7 +44,7 @@ public class UnboundIdContainer
 
 	private InMemoryDirectoryServer directoryServer;
 
-	private String defaultPartitionSuffix;
+	private final String defaultPartitionSuffix;
 
 	private int port = 53389;
 
@@ -51,7 +52,7 @@ public class UnboundIdContainer
 
 	private boolean running;
 
-	private String ldif;
+	private final String ldif;
 
 	public UnboundIdContainer(String defaultPartitionSuffix, String ldif) {
 		this.defaultPartitionSuffix = defaultPartitionSuffix;
@@ -79,7 +80,7 @@ public class UnboundIdContainer
 	}
 
 	@Override
-	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+	public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {
 		this.context = applicationContext;
 	}
 

+ 2 - 12
ldap/src/main/java/org/springframework/security/ldap/userdetails/DefaultLdapAuthoritiesPopulator.java

@@ -129,7 +129,7 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
 	/**
 	 * The base DN from which the search for group membership should be performed
 	 */
-	private String groupSearchBase;
+	private final String groupSearchBase;
 
 	/**
 	 * The pattern to be used for the user search. {0} is the user's DN
@@ -166,7 +166,7 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
 		if (groupSearchBase == null) {
 			logger.info("Will not perform group search since groupSearchBase is null.");
 		}
-		else if (groupSearchBase.length() == 0) {
+		else if (groupSearchBase.isEmpty()) {
 			logger.info("Will perform group search from the context source base since groupSearchBase is empty.");
 		}
 		this.authorityMapper = (record) -> {
@@ -365,16 +365,6 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
 		return this.convertToUpperCase;
 	}
 
-	/**
-	 * Returns the default role Method available so that classes extending this can
-	 * override
-	 * @return the default role used
-	 * @see #setDefaultRole(String)
-	 */
-	private GrantedAuthority getDefaultRole() {
-		return this.defaultRole;
-	}
-
 	/**
 	 * Returns the search controls Method available so that classes extending this can
 	 * override the search controls used

+ 11 - 11
ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapAuthority.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2023 the original author or authors.
+ * Copyright 2002-2024 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -31,16 +31,16 @@ import org.springframework.util.Assert;
  */
 public class LdapAuthority implements GrantedAuthority {
 
-	private String dn;
+	private final String dn;
 
-	private String role;
+	private final String role;
 
-	private Map<String, List<String>> attributes;
+	private final Map<String, List<String>> attributes;
 
 	/**
 	 * Constructs an LdapAuthority that has a role and a DN but no other attributes
-	 * @param role
-	 * @param dn
+	 * @param role the principal's role
+	 * @param dn the distinguished name
 	 */
 	public LdapAuthority(String role, String dn) {
 		this(role, dn, null);
@@ -48,9 +48,9 @@ public class LdapAuthority implements GrantedAuthority {
 
 	/**
 	 * Constructs an LdapAuthority with the given role, DN and other LDAP attributes
-	 * @param role
-	 * @param dn
-	 * @param attributes
+	 * @param role the principal's role
+	 * @param dn the distinguished name
+	 * @param attributes additional LDAP attributes
 	 */
 	public LdapAuthority(String role, String dn, Map<String, List<String>> attributes) {
 		Assert.notNull(role, "role can not be null");
@@ -70,7 +70,7 @@ public class LdapAuthority implements GrantedAuthority {
 
 	/**
 	 * Returns the DN for this LDAP authority
-	 * @return
+	 * @return the distinguished name
 	 */
 	public String getDn() {
 		return this.dn;
@@ -91,7 +91,7 @@ public class LdapAuthority implements GrantedAuthority {
 
 	/**
 	 * Returns the first attribute value for a specified attribute
-	 * @param name
+	 * @param name the attribute name
 	 * @return the first attribute value for a specified attribute, may be null
 	 */
 	public String getFirstAttributeValue(String name) {

+ 16 - 8
ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsManager.java

@@ -44,7 +44,6 @@ import org.apache.commons.logging.LogFactory;
 import org.springframework.core.log.LogMessage;
 import org.springframework.ldap.core.AttributesMapper;
 import org.springframework.ldap.core.AttributesMapperCallbackHandler;
-import org.springframework.ldap.core.ContextExecutor;
 import org.springframework.ldap.core.ContextSource;
 import org.springframework.ldap.core.DirContextAdapter;
 import org.springframework.ldap.core.DistinguishedName;
@@ -121,7 +120,7 @@ public class LdapUserDetailsManager implements UserDetailsManager {
 	private final LdapTemplate template;
 
 	/** Default context mapper used to create a set of roles from a list of attributes */
-	private AttributesMapper roleMapper = (attributes) -> {
+	private AttributesMapper<GrantedAuthority> roleMapper = (attributes) -> {
 		Attribute roleAttr = attributes.get(this.groupRoleAttributeName);
 		NamingEnumeration<?> ne = roleAttr.getAll();
 		Object group = ne.next();
@@ -147,7 +146,7 @@ public class LdapUserDetailsManager implements UserDetailsManager {
 	}
 
 	private DirContextAdapter loadUserAsContext(final LdapName dn, final String username) {
-		return (DirContextAdapter) this.template.executeReadOnly((ContextExecutor) (ctx) -> {
+		return this.template.executeReadOnly((ctx) -> {
 			try {
 				Attributes attrs = ctx.getAttributes(dn, this.attributesToRetrieve);
 				return new DirContextAdapter(attrs, LdapUtils.getFullDn(dn, ctx));
@@ -162,6 +161,7 @@ public class LdapUserDetailsManager implements UserDetailsManager {
 	 * Changes the password for the current user. The username is obtained from the
 	 * security context.
 	 *
+	 * <p>
 	 * There are two supported strategies for modifying the user's password depending on
 	 * the capabilities of the corresponding LDAP server.
 	 *
@@ -170,6 +170,7 @@ public class LdapUserDetailsManager implements UserDetailsManager {
 	 * <a target="_blank" href="https://tools.ietf.org/html/rfc3062"> LDAP Password Modify
 	 * Extended Operation </a>.
 	 *
+	 * <p>
 	 * See {@link LdapUserDetailsManager#setUsePasswordModifyExtensionOperation(boolean)}
 	 * for details.
 	 * </p>
@@ -205,7 +206,6 @@ public class LdapUserDetailsManager implements UserDetailsManager {
 	 * @param username the user whose roles are required.
 	 * @return the granted authorities returned by the group search
 	 */
-	@SuppressWarnings("unchecked")
 	List<GrantedAuthority> getUserAuthorities(final LdapName dn, final String username) {
 		SearchExecutor se = (ctx) -> {
 			LdapName fullDn = LdapUtils.getFullDn(dn, ctx);
@@ -214,7 +214,8 @@ public class LdapUserDetailsManager implements UserDetailsManager {
 			return ctx.search(this.groupSearchBase, this.groupSearchFilter,
 					new String[] { fullDn.toString(), username }, ctrls);
 		};
-		AttributesMapperCallbackHandler roleCollector = new AttributesMapperCallbackHandler(this.roleMapper);
+		AttributesMapperCallbackHandler<GrantedAuthority> roleCollector = new AttributesMapperCallbackHandler<>(
+				this.roleMapper);
 		this.template.search(se, roleCollector);
 		return roleCollector.getList();
 	}
@@ -229,7 +230,7 @@ public class LdapUserDetailsManager implements UserDetailsManager {
 		// Check for any existing authorities which might be set for this
 		// DN and remove them
 		List<GrantedAuthority> authorities = getUserAuthorities(dn, user.getUsername());
-		if (authorities.size() > 0) {
+		if (!authorities.isEmpty()) {
 			removeAuthorities(dn, authorities);
 		}
 		addAuthorities(dn, user.getAuthorities());
@@ -322,7 +323,7 @@ public class LdapUserDetailsManager implements UserDetailsManager {
 
 	private void modifyAuthorities(final LdapName userDn, final Collection<? extends GrantedAuthority> authorities,
 			final int modType) {
-		this.template.executeReadWrite((ContextExecutor) (ctx) -> {
+		this.template.executeReadWrite((ctx) -> {
 			for (GrantedAuthority authority : authorities) {
 				String group = convertAuthorityToGroup(authority);
 				LdapName fullDn = LdapUtils.getFullDn(userDn, ctx);
@@ -389,20 +390,26 @@ public class LdapUserDetailsManager implements UserDetailsManager {
 	/**
 	 * Sets the method by which a user's password gets modified.
 	 *
+	 * <p>
 	 * If set to {@code true}, then {@link LdapUserDetailsManager#changePassword} will
 	 * modify the user's password by way of the
 	 * <a target="_blank" href="https://tools.ietf.org/html/rfc3062">Password Modify
 	 * Extension Operation</a>.
 	 *
+	 * <p>
 	 * If set to {@code false}, then {@link LdapUserDetailsManager#changePassword} will
 	 * modify the user's password by directly modifying attributes on the corresponding
 	 * entry.
 	 *
+	 * <p>
 	 * Before using this setting, ensure that the corresponding LDAP server supports this
 	 * extended operation.
 	 *
+	 * <p>
 	 * By default, {@code usePasswordModifyExtensionOperation} is false.
-	 * @param usePasswordModifyExtensionOperation
+	 * @param usePasswordModifyExtensionOperation whether to use the
+	 * <a target="_blank" href="https://tools.ietf.org/html/rfc3062">Password Modify
+	 * Extension Operation</a> to modify the password
 	 * @since 4.2.9
 	 */
 	public void setUsePasswordModifyExtensionOperation(boolean usePasswordModifyExtensionOperation) {
@@ -473,6 +480,7 @@ public class LdapUserDetailsManager implements UserDetailsManager {
 	 * <a target="_blank" href="https://tools.ietf.org/html/rfc3062"> LDAP Password Modify
 	 * Extended Operation </a> client request.
 	 *
+	 * <p>
 	 * Can be directed at any LDAP server that supports the Password Modify Extended
 	 * Operation.
 	 *

+ 3 - 3
ldap/src/main/java/org/springframework/security/ldap/userdetails/NestedLdapAuthoritiesPopulator.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2024 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -176,7 +176,7 @@ public class NestedLdapAuthoritiesPopulator extends DefaultLdapAuthoritiesPopula
 		if (getAttributeNames() == null) {
 			setAttributeNames(new HashSet<>());
 		}
-		if (StringUtils.hasText(getGroupRoleAttribute()) && !getAttributeNames().contains(getGroupRoleAttribute())) {
+		if (StringUtils.hasText(getGroupRoleAttribute())) {
 			getAttributeNames().add(getGroupRoleAttribute());
 		}
 		Set<Map<String, List<String>>> userRoles = getLdapTemplate().searchForMultipleAttributeValues(
@@ -200,7 +200,7 @@ public class NestedLdapAuthoritiesPopulator extends DefaultLdapAuthoritiesPopula
 				// this prevents a forever loop for a misconfigured ldap directory
 				circular = circular | (!authorities.add(new LdapAuthority(role, dn, record)));
 			}
-			String roleName = (roles.size() > 0) ? roles.iterator().next() : dn;
+			String roleName = (!roles.isEmpty()) ? roles.iterator().next() : dn;
 			if (!circular) {
 				performNestedSearch(dn, roleName, authorities, (depth - 1));
 			}

+ 1 - 1
ldap/src/main/java/org/springframework/security/ldap/userdetails/UserDetailsContextMapper.java

@@ -39,7 +39,7 @@ public interface UserDetailsContextMapper {
 	 * Creates a fully populated UserDetails object for use by the security framework.
 	 * @param ctx the context object which contains the user information.
 	 * @param username the user's supplied login name.
-	 * @param authorities
+	 * @param authorities the authorities to add to the {@code UserDetails} instance
 	 * @return the user object.
 	 */
 	UserDetails mapUserFromContext(DirContextOperations ctx, String username,