|
|
|
@ -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"); |
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
|
|
|
* you may not use this file except in compliance with the License. |
|
|
|
* you may not use this file except in compliance with the License. |
|
|
|
@ -17,12 +17,9 @@ |
|
|
|
package org.springframework.security.ldap.authentication.ad; |
|
|
|
package org.springframework.security.ldap.authentication.ad; |
|
|
|
|
|
|
|
|
|
|
|
import java.io.Serializable; |
|
|
|
import java.io.Serializable; |
|
|
|
import java.util.ArrayList; |
|
|
|
|
|
|
|
import java.util.Arrays; |
|
|
|
|
|
|
|
import java.util.Collection; |
|
|
|
import java.util.Collection; |
|
|
|
import java.util.HashMap; |
|
|
|
import java.util.HashMap; |
|
|
|
import java.util.Hashtable; |
|
|
|
import java.util.Hashtable; |
|
|
|
import java.util.List; |
|
|
|
|
|
|
|
import java.util.Map; |
|
|
|
import java.util.Map; |
|
|
|
import java.util.regex.Matcher; |
|
|
|
import java.util.regex.Matcher; |
|
|
|
import java.util.regex.Pattern; |
|
|
|
import java.util.regex.Pattern; |
|
|
|
@ -39,7 +36,6 @@ import org.springframework.core.log.LogMessage; |
|
|
|
import org.springframework.dao.IncorrectResultSizeDataAccessException; |
|
|
|
import org.springframework.dao.IncorrectResultSizeDataAccessException; |
|
|
|
import org.springframework.ldap.CommunicationException; |
|
|
|
import org.springframework.ldap.CommunicationException; |
|
|
|
import org.springframework.ldap.core.DirContextOperations; |
|
|
|
import org.springframework.ldap.core.DirContextOperations; |
|
|
|
import org.springframework.ldap.core.DistinguishedName; |
|
|
|
|
|
|
|
import org.springframework.ldap.core.support.DefaultDirObjectFactory; |
|
|
|
import org.springframework.ldap.core.support.DefaultDirObjectFactory; |
|
|
|
import org.springframework.ldap.support.LdapUtils; |
|
|
|
import org.springframework.ldap.support.LdapUtils; |
|
|
|
import org.springframework.security.authentication.AccountExpiredException; |
|
|
|
import org.springframework.security.authentication.AccountExpiredException; |
|
|
|
@ -50,11 +46,10 @@ import org.springframework.security.authentication.InternalAuthenticationService |
|
|
|
import org.springframework.security.authentication.LockedException; |
|
|
|
import org.springframework.security.authentication.LockedException; |
|
|
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; |
|
|
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; |
|
|
|
import org.springframework.security.core.GrantedAuthority; |
|
|
|
import org.springframework.security.core.GrantedAuthority; |
|
|
|
import org.springframework.security.core.authority.AuthorityUtils; |
|
|
|
|
|
|
|
import org.springframework.security.core.authority.SimpleGrantedAuthority; |
|
|
|
|
|
|
|
import org.springframework.security.core.userdetails.UsernameNotFoundException; |
|
|
|
import org.springframework.security.core.userdetails.UsernameNotFoundException; |
|
|
|
import org.springframework.security.ldap.SpringSecurityLdapTemplate; |
|
|
|
import org.springframework.security.ldap.SpringSecurityLdapTemplate; |
|
|
|
import org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider; |
|
|
|
import org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider; |
|
|
|
|
|
|
|
import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator; |
|
|
|
import org.springframework.util.Assert; |
|
|
|
import org.springframework.util.Assert; |
|
|
|
import org.springframework.util.StringUtils; |
|
|
|
import org.springframework.util.StringUtils; |
|
|
|
|
|
|
|
|
|
|
|
@ -72,9 +67,9 @@ import org.springframework.util.StringUtils; |
|
|
|
* <p> |
|
|
|
* <p> |
|
|
|
* The user authorities are obtained from the data contained in the {@code memberOf} |
|
|
|
* The user authorities are obtained from the data contained in the {@code memberOf} |
|
|
|
* attribute. |
|
|
|
* attribute. |
|
|
|
* |
|
|
|
* <p> |
|
|
|
* <h3>Active Directory Sub-Error Codes</h3> |
|
|
|
* <h3>Active Directory Sub-Error Codes</h3> |
|
|
|
* |
|
|
|
* <p> |
|
|
|
* When an authentication fails, resulting in a standard LDAP 49 error code, Active |
|
|
|
* When an authentication fails, resulting in a standard LDAP 49 error code, Active |
|
|
|
* Directory also supplies its own sub-error codes within the error message. These will be |
|
|
|
* Directory also supplies its own sub-error codes within the error message. These will be |
|
|
|
* used to provide additional log information on why an authentication has failed. Typical |
|
|
|
* used to provide additional log information on why an authentication has failed. Typical |
|
|
|
@ -90,13 +85,14 @@ import org.springframework.util.StringUtils; |
|
|
|
* <li>773 - user must reset password</li> |
|
|
|
* <li>773 - user must reset password</li> |
|
|
|
* <li>775 - account locked</li> |
|
|
|
* <li>775 - account locked</li> |
|
|
|
* </ul> |
|
|
|
* </ul> |
|
|
|
* |
|
|
|
* <p> |
|
|
|
* If you set the {@link #setConvertSubErrorCodesToExceptions(boolean) |
|
|
|
* If you set the {@link #setConvertSubErrorCodesToExceptions(boolean) |
|
|
|
* convertSubErrorCodesToExceptions} property to {@code true}, the codes will also be used |
|
|
|
* convertSubErrorCodesToExceptions} property to {@code true}, the codes will also be used |
|
|
|
* to control the exception raised. |
|
|
|
* to control the exception raised. |
|
|
|
* |
|
|
|
* |
|
|
|
* @author Luke Taylor |
|
|
|
* @author Luke Taylor |
|
|
|
* @author Rob Winch |
|
|
|
* @author Rob Winch |
|
|
|
|
|
|
|
* @author Roman Zabaluev |
|
|
|
* @since 3.1 |
|
|
|
* @since 3.1 |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLdapAuthenticationProvider { |
|
|
|
public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLdapAuthenticationProvider { |
|
|
|
@ -135,20 +131,23 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda |
|
|
|
// Only used to allow tests to substitute a mock LdapContext
|
|
|
|
// Only used to allow tests to substitute a mock LdapContext
|
|
|
|
ContextFactory contextFactory = new ContextFactory(); |
|
|
|
ContextFactory contextFactory = new ContextFactory(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private LdapAuthoritiesPopulator authoritiesPopulator = new DefaultActiveDirectoryAuthoritiesPopulator(); |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* @param domain the domain name (may be null or empty) |
|
|
|
* @param domain the domain name (can be null or empty) |
|
|
|
* @param url an LDAP url (or multiple URLs) |
|
|
|
* @param url an LDAP url (or multiple URLs) |
|
|
|
* @param rootDn the root DN (may be null or empty) |
|
|
|
* @param rootDn the root DN (can be null or empty) |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
public ActiveDirectoryLdapAuthenticationProvider(String domain, String url, String rootDn) { |
|
|
|
public ActiveDirectoryLdapAuthenticationProvider(String domain, String url, String rootDn) { |
|
|
|
Assert.isTrue(StringUtils.hasText(url), "Url cannot be empty"); |
|
|
|
Assert.isTrue(StringUtils.hasText(url), "Url cannot be empty"); |
|
|
|
this.domain = StringUtils.hasText(domain) ? domain.toLowerCase() : null; |
|
|
|
this.domain = StringUtils.hasText(domain) ? domain.toLowerCase() : null; |
|
|
|
this.url = url; |
|
|
|
this.url = url; |
|
|
|
this.rootDn = StringUtils.hasText(rootDn) ? rootDn.toLowerCase() : null; |
|
|
|
this.rootDn = StringUtils.hasText(rootDn) ? rootDn.toLowerCase() : null; |
|
|
|
|
|
|
|
this.setAuthoritiesPopulator(this.authoritiesPopulator); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* @param domain the domain name (may be null or empty) |
|
|
|
* @param domain the domain name (can be null or empty) |
|
|
|
* @param url an LDAP url (or multiple URLs) |
|
|
|
* @param url an LDAP url (or multiple URLs) |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
public ActiveDirectoryLdapAuthenticationProvider(String domain, String url) { |
|
|
|
public ActiveDirectoryLdapAuthenticationProvider(String domain, String url) { |
|
|
|
@ -156,6 +155,7 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda |
|
|
|
this.domain = StringUtils.hasText(domain) ? domain.toLowerCase() : null; |
|
|
|
this.domain = StringUtils.hasText(domain) ? domain.toLowerCase() : null; |
|
|
|
this.url = url; |
|
|
|
this.url = url; |
|
|
|
this.rootDn = (this.domain != null) ? rootDnFromDomain(this.domain) : null; |
|
|
|
this.rootDn = (this.domain != null) ? rootDnFromDomain(this.domain) : null; |
|
|
|
|
|
|
|
this.setAuthoritiesPopulator(this.authoritiesPopulator); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@Override |
|
|
|
@Override |
|
|
|
@ -179,26 +179,10 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Creates the user authority list from the values of the {@code memberOf} attribute |
|
|
|
|
|
|
|
* obtained from the user's Active Directory entry. |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
@Override |
|
|
|
@Override |
|
|
|
protected Collection<? extends GrantedAuthority> loadUserAuthorities(DirContextOperations userData, String username, |
|
|
|
protected Collection<? extends GrantedAuthority> loadUserAuthorities(DirContextOperations userData, String username, |
|
|
|
String password) { |
|
|
|
String password) { |
|
|
|
String[] groups = userData.getStringAttributes("memberOf"); |
|
|
|
return this.authoritiesPopulator.getGrantedAuthorities(userData, username); |
|
|
|
if (groups == null) { |
|
|
|
|
|
|
|
this.logger.debug("No values for 'memberOf' attribute."); |
|
|
|
|
|
|
|
return AuthorityUtils.NO_AUTHORITIES; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (this.logger.isDebugEnabled()) { |
|
|
|
|
|
|
|
this.logger.debug("'memberOf' attribute values: " + Arrays.asList(groups)); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
List<GrantedAuthority> authorities = new ArrayList<>(groups.length); |
|
|
|
|
|
|
|
for (String group : groups) { |
|
|
|
|
|
|
|
authorities.add(new SimpleGrantedAuthority(new DistinguishedName(group).removeLast().getValue())); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return authorities; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private DirContext bindAsUser(String username, String password) { |
|
|
|
private DirContext bindAsUser(String username, String password) { |
|
|
|
@ -332,14 +316,14 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda |
|
|
|
+ "' does not contain the domain, and no domain has been configured"); |
|
|
|
+ "' does not contain the domain, and no domain has been configured"); |
|
|
|
throw badCredentials(); |
|
|
|
throw badCredentials(); |
|
|
|
} |
|
|
|
} |
|
|
|
return rootDnFromDomain(bindPrincipal.substring(atChar + 1, bindPrincipal.length())); |
|
|
|
return rootDnFromDomain(bindPrincipal.substring(atChar + 1)); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private String rootDnFromDomain(String domain) { |
|
|
|
private String rootDnFromDomain(String domain) { |
|
|
|
String[] tokens = StringUtils.tokenizeToStringArray(domain, "."); |
|
|
|
String[] tokens = StringUtils.tokenizeToStringArray(domain, "."); |
|
|
|
StringBuilder root = new StringBuilder(); |
|
|
|
StringBuilder root = new StringBuilder(); |
|
|
|
for (String token : tokens) { |
|
|
|
for (String token : tokens) { |
|
|
|
if (root.length() > 0) { |
|
|
|
if (!root.isEmpty()) { |
|
|
|
root.append(','); |
|
|
|
root.append(','); |
|
|
|
} |
|
|
|
} |
|
|
|
root.append("dc=").append(token); |
|
|
|
root.append("dc=").append(token); |
|
|
|
@ -379,7 +363,6 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda |
|
|
|
* Defaults to: {@code (&(objectClass=user)(userPrincipalName={0}))} |
|
|
|
* Defaults to: {@code (&(objectClass=user)(userPrincipalName={0}))} |
|
|
|
* </p> |
|
|
|
* </p> |
|
|
|
* @param searchFilter the filter string |
|
|
|
* @param searchFilter the filter string |
|
|
|
* |
|
|
|
|
|
|
|
* @since 3.2.6 |
|
|
|
* @since 3.2.6 |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
public void setSearchFilter(String searchFilter) { |
|
|
|
public void setSearchFilter(String searchFilter) { |
|
|
|
@ -397,6 +380,19 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda |
|
|
|
this.contextEnvironmentProperties = new Hashtable<>(environment); |
|
|
|
this.contextEnvironmentProperties = new Hashtable<>(environment); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Set the strategy for obtaining the authorities for a given user after they've been |
|
|
|
|
|
|
|
* authenticated. Consider adjusting this if you require a custom authorities mapping |
|
|
|
|
|
|
|
* algorithm different from a default one. The default value is |
|
|
|
|
|
|
|
* DefaultActiveDirectoryAuthoritiesPopulator. |
|
|
|
|
|
|
|
* @param authoritiesPopulator authorities population strategy |
|
|
|
|
|
|
|
* @since 6.3 |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
public void setAuthoritiesPopulator(LdapAuthoritiesPopulator authoritiesPopulator) { |
|
|
|
|
|
|
|
Assert.notNull(authoritiesPopulator, "An LdapAuthoritiesPopulator must be supplied"); |
|
|
|
|
|
|
|
this.authoritiesPopulator = authoritiesPopulator; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
static class ContextFactory { |
|
|
|
static class ContextFactory { |
|
|
|
|
|
|
|
|
|
|
|
DirContext createContext(Hashtable<?, ?> env) throws NamingException { |
|
|
|
DirContext createContext(Hashtable<?, ?> env) throws NamingException { |
|
|
|
|