LdaptiveAuthenticationManager.java
/*
* Copyright 2014 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.
* 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.bremersee.spring.security.ldaptive.authentication;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import static java.util.Objects.requireNonNullElseGet;
import static org.springframework.util.ObjectUtils.isEmpty;
import java.util.Collection;
import java.util.Optional;
import java.util.stream.Stream;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.bremersee.ldaptive.DefaultLdaptiveErrorHandler;
import org.bremersee.ldaptive.LdaptiveTemplate;
import org.bremersee.spring.security.core.EmailToUsernameResolver;
import org.bremersee.spring.security.ldaptive.authentication.provider.NoAccountControlEvaluator;
import org.bremersee.spring.security.ldaptive.userdetails.LdaptiveRememberMeTokenProvider;
import org.bremersee.spring.security.ldaptive.userdetails.LdaptiveUserDetails;
import org.bremersee.spring.security.ldaptive.userdetails.LdaptiveUserDetailsService;
import org.jspecify.annotations.NonNull;
import org.ldaptive.BindOperation;
import org.ldaptive.BindResponse;
import org.ldaptive.CompareRequest;
import org.ldaptive.ConnectionConfig;
import org.ldaptive.ConnectionFactory;
import org.ldaptive.DefaultConnectionFactory;
import org.ldaptive.LdapException;
import org.ldaptive.SimpleBindRequest;
import org.ldaptive.SingleConnectionFactory;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.RememberMeAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.Assert;
/**
* The ldaptive authentication manager.
*
* @author Christian Bremer
*/
public class LdaptiveAuthenticationManager
implements AuthenticationManager, AuthenticationProvider, MessageSourceAware {
/**
* The Logger.
*/
protected final Log logger = LogFactory.getLog(this.getClass());
/**
* The authentication properties.
*/
@Getter(AccessLevel.PROTECTED)
private final LdaptiveAuthenticationProperties authenticationProperties;
/**
* The remember-me key.
*/
@Getter(AccessLevel.PROTECTED)
private final String rememberMeKey;
/**
* The application ldaptive template.
*/
@Getter(AccessLevel.PROTECTED)
private final LdaptiveTemplate applicationLdaptiveTemplate;
/**
* The email to username resolver.
*/
@Getter(AccessLevel.PROTECTED)
private EmailToUsernameResolver emailToUsernameResolver;
/**
* The password encoder.
*/
@Getter(AccessLevel.PROTECTED)
@Setter
private PasswordEncoder passwordEncoder;
/**
* The account control evaluator.
*/
@Getter(AccessLevel.PROTECTED)
private AccountControlEvaluator accountControlEvaluator;
/**
* The groups mapper.
*/
@Getter(AccessLevel.PROTECTED)
@Setter
private GrantedAuthoritiesMapper grantedAuthoritiesMapper;
/**
* The remember-me token provider.
*/
@Getter(AccessLevel.PROTECTED)
@Setter
private LdaptiveRememberMeTokenProvider passwordProvider;
/**
* The token converter.
*/
@Getter(AccessLevel.PROTECTED)
@Setter
private Converter<LdaptiveUserDetails, LdaptiveAuthentication> tokenConverter;
/**
* The message source.
*/
@Getter(AccessLevel.PROTECTED)
private MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
/**
* Instantiates a new ldaptive authentication manager.
*
* @param connectionConfig the connection config
* @param authenticationProperties the authentication properties
* @param rememberMeKey the remember me key
*/
public LdaptiveAuthenticationManager(
ConnectionConfig connectionConfig,
LdaptiveAuthenticationProperties authenticationProperties,
String rememberMeKey) {
this(new DefaultConnectionFactory(connectionConfig), authenticationProperties, rememberMeKey);
}
/**
* Instantiates a new ldaptive authentication manager.
*
* @param connectionFactory the connection factory
* @param authenticationProperties the authentication properties
* @param rememberMeKey the remember me key
*/
public LdaptiveAuthenticationManager(
ConnectionFactory connectionFactory,
LdaptiveAuthenticationProperties authenticationProperties,
String rememberMeKey) {
this(new LdaptiveTemplate(connectionFactory), authenticationProperties, rememberMeKey);
}
/**
* Instantiates a new ldaptive authentication manager.
*
* @param applicationLdaptiveTemplate the application ldaptive template
* @param authenticationProperties the authentication properties
* @param rememberMeKey the remember me key
*/
public LdaptiveAuthenticationManager(
LdaptiveTemplate applicationLdaptiveTemplate,
LdaptiveAuthenticationProperties authenticationProperties,
String rememberMeKey) {
this.applicationLdaptiveTemplate = applicationLdaptiveTemplate;
Assert.notNull(getApplicationLdaptiveTemplate(), "Application ldaptive template is required.");
this.authenticationProperties = authenticationProperties;
Assert.notNull(getAuthenticationProperties(), "Authentication properties are required.");
this.rememberMeKey = rememberMeKey;
// emailToUsernameResolver
setEmailToUsernameResolver(new EmailToUsernameResolverByLdapAttribute(
getAuthenticationProperties(), getApplicationLdaptiveTemplate()));
// accountControlEvaluator
if (isNull(getAuthenticationProperties().getAccountControlEvaluator())) {
setAccountControlEvaluator(new NoAccountControlEvaluator());
} else {
setAccountControlEvaluator(getAuthenticationProperties().getAccountControlEvaluator().get());
}
}
/**
* Sets email to username resolver.
*
* @param emailToUsernameResolver the email to username resolver
*/
public void setEmailToUsernameResolver(
EmailToUsernameResolver emailToUsernameResolver) {
if (nonNull(emailToUsernameResolver)) {
this.emailToUsernameResolver = emailToUsernameResolver;
}
}
/**
* Sets account control evaluator.
*
* @param accountControlEvaluator the account control evaluator
*/
public void setAccountControlEvaluator(
AccountControlEvaluator accountControlEvaluator) {
if (nonNull(accountControlEvaluator)) {
this.accountControlEvaluator = accountControlEvaluator;
}
}
@Override
public void setMessageSource(@NonNull MessageSource messageSource) {
this.messages = new MessageSourceAccessor(messageSource);
}
/**
* Init.
*/
public void init() {
if (!isSimpleBindAuthentication() && isNull(getPasswordEncoder())) {
throw new IllegalStateException(String.format("A password attribute is set (%s) but no "
+ "password encoder is present. Either delete the password attribute to perform a "
+ "bind to authenticate or set a password encoder.",
getAuthenticationProperties().getPasswordAttribute()));
}
}
@Override
public boolean supports(@NonNull Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication)
|| isRememberMeAuthentication(authentication);
}
private boolean isRememberMeAuthentication(Class<?> authentication) {
return !isEmpty(getRememberMeKey())
&& RememberMeAuthenticationToken.class.isAssignableFrom(authentication);
}
/**
* Remember me key matches given authentication.
*
* @param authentication the authentication
* @return the boolean
*/
protected boolean rememberMeKeyMatches(RememberMeAuthenticationToken authentication) {
return Optional.ofNullable(getRememberMeKey())
.filter(key -> key.hashCode() == authentication.getKeyHash())
.isPresent();
}
@NonNull
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
if (!supports(authentication.getClass())) {
logger.debug(String.format("Authentication [%s] is not supported.",
authentication.getClass().getName()));
//noinspection DataFlowIssue
return null;
}
if (authentication instanceof RememberMeAuthenticationToken rma) {
if (!rememberMeKeyMatches(rma)) {
throw new BadCredentialsException(getMessages().getMessage(
"RememberMeAuthenticationProvider.incorrectKey",
"The presented RememberMeAuthenticationToken does not contain the expected key"));
}
return rma;
}
String name = getName(authentication);
logger.debug("Authenticating user '" + name + "' ...");
String password = Optional.ofNullable(authentication.getCredentials())
.map(String::valueOf)
.orElseThrow(() -> new BadCredentialsException("Password is required."));
String username = getEmailToUsernameResolver()
.getUsernameByEmail(name)
.orElse(name);
if (isRefusedUsername(username)) {
throw new DisabledException(String
.format("Username '%s' is refused by configuration.", username));
}
LdaptiveUserDetails userDetails = getUserDetailsService().loadUserByUsername(username);
checkPassword(userDetails, password);
checkAccountControl(userDetails);
if (nonNull(getTokenConverter())) {
return getTokenConverter().convert(userDetails);
}
return new LdaptiveAuthenticationToken(userDetails);
}
/**
* Determines whether the username is refused by configuration.
*
* @param username the username
* @return {@code true} if the username is refused, otherwise {@code false}
*/
protected boolean isRefusedUsername(String username) {
if (isEmpty(username)) {
return true;
}
return Stream.ofNullable(getAuthenticationProperties().getRefusedUsernames())
.flatMap(Collection::stream)
.filter(refusedUsername -> !isEmpty(refusedUsername))
.anyMatch(refusedUsername -> refusedUsername.equalsIgnoreCase(username));
}
/**
* Gets name.
*
* @param authentication the authentication
* @return the name
*/
protected String getName(Authentication authentication) {
Object principal = authentication.getPrincipal();
if (principal instanceof LdaptiveUserDetails ldaptiveUserDetails) {
return requireNonNullElseGet(ldaptiveUserDetails.getDn(), authentication::getName);
}
return authentication.getName();
}
/**
* Gets user details service.
*
* @return the user details service
*/
public LdaptiveUserDetailsService getUserDetailsService() {
LdaptiveUserDetailsService userDetailsService = new LdaptiveUserDetailsService(
getAuthenticationProperties(), getApplicationLdaptiveTemplate());
userDetailsService.setAccountControlEvaluator(getAccountControlEvaluator());
userDetailsService.setGrantedAuthoritiesMapper(getGrantedAuthoritiesMapper());
userDetailsService.setRememberMeTokenProvider(getPasswordProvider());
return userDetailsService;
}
/**
* Determines whether to bind with username and password or to compare the passwords.
*
* @return the boolean
*/
protected boolean isSimpleBindAuthentication() {
return isNull(getAuthenticationProperties().getPasswordAttribute())
|| getAuthenticationProperties().getPasswordAttribute().isBlank();
}
/**
* Check password.
*
* @param user the user
* @param password the password
*/
protected void checkPassword(LdaptiveUserDetails user, String password) {
if (isSimpleBindAuthentication()) {
checkPasswordWithSimpleBind(user, password);
} else {
checkPasswordWithCompareRequest(user, password);
}
}
/**
* Check password with compare request.
*
* @param user the user
* @param password the password
*/
protected void checkPasswordWithCompareRequest(LdaptiveUserDetails user, String password) {
Assert.notNull(getPasswordEncoder(), "No password encoder is present.");
boolean matches = getApplicationLdaptiveTemplate().compare(CompareRequest.builder()
.dn(user.getDn())
.name(getAuthenticationProperties().getPasswordAttribute())
.value(getPasswordEncoder().encode(password))
.build());
if (!matches) {
throw new BadCredentialsException("Password doesn't match.");
}
}
/**
* Check password with simple bind.
*
* @param user the user
* @param password the password
*/
protected void checkPasswordWithSimpleBind(LdaptiveUserDetails user, String password) {
SingleConnectionFactory connectionFactory = getSingleConnectionFactory();
try {
connectionFactory.initialize();
BindOperation bind = getBindOperation(connectionFactory);
BindResponse response = bind.execute(SimpleBindRequest.builder()
.dn(user.getDn())
.password(password)
.build());
if (!response.isSuccess()) {
throw new BadCredentialsException("Password doesn't match.");
}
} catch (LdapException ldapException) {
new DefaultLdaptiveErrorHandler().handleError(ldapException);
} finally {
connectionFactory.close();
}
}
/**
* Gets single connection factory.
*
* @return the single connection factory
*/
SingleConnectionFactory getSingleConnectionFactory() {
ConnectionConfig connectionConfig = ConnectionConfig
.copy(getApplicationLdaptiveTemplate().getConnectionFactory().getConnectionConfig());
connectionConfig.setConnectionInitializers();
return new SingleConnectionFactory(connectionConfig);
}
/**
* Gets bind operation.
*
* @param cf the cf
* @return the bind operation
*/
BindOperation getBindOperation(SingleConnectionFactory cf) {
return new BindOperation(cf);
}
/**
* Check account control.
*
* @param user the user
*/
protected void checkAccountControl(LdaptiveUserDetails user) {
if (!user.isEnabled()) {
throw new DisabledException("Account is disabled.");
}
if (!user.isAccountNonLocked()) {
throw new LockedException("Account is locked.");
}
if (!user.isAccountNonExpired()) {
throw new AccountExpiredException("Account is expired.");
}
if (!user.isCredentialsNonExpired()) {
throw new CredentialsExpiredException("Credentials are expired.");
}
}
}