/*******************************************************************************
* Copyright (c) 2011 GigaSpaces Technologies Ltd. All rights reserved
*
* 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.cloudifysource.securityldap;
import java.util.Collection;
import java.util.logging.Logger;
import org.cloudifysource.security.CustomAuthenticationToken;
import org.springframework.context.MessageSource;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.ldap.NamingException;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.ldap.authentication.LdapAuthenticator;
import org.springframework.security.ldap.ppolicy.PasswordPolicyException;
import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* This is a custom LdapAuthenticationProvider that supports authorization groups on top of authorities (roles).
* @author noak
* @since 2.3.0
*
*/
public class CustomLdapAuthenticationProvider implements AuthenticationProvider {
private Logger logger = java.util.logging.Logger.getLogger(CustomLdapAuthenticationProvider.class.getName());
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private LdapAuthenticator authenticator;
private LdapAuthoritiesPopulator authoritiesPopulator;
private LdapAuthGroupsPopulator authGroupsPopulator;
private CustomLdapUserDetailsMapper userDetailsContextMapper = new CustomLdapUserDetailsMapper();
private boolean useAuthenticationRequestCredentials = true;
private boolean hideUserNotFoundExceptions = true;
/**
* Create an instance with the supplied authenticator and authorities populator implementations.
*
* @param authenticator the authentication strategy (bind, password comparison, etc)
* to be used by this provider for authenticating users.
* @param authoritiesPopulator the strategy for obtaining the authorities for a given user after they've been
* authenticated.
*/
public CustomLdapAuthenticationProvider(final LdapAuthenticator authenticator,
final LdapAuthoritiesPopulator authoritiesPopulator,
final LdapAuthGroupsPopulator authGroupsPopulator) {
logger.finest("CustomLdapAuthenticationProvider : constructor");
this.setAuthenticator(authenticator);
this.setAuthoritiesPopulator(authoritiesPopulator);
this.setAuthGroupsPopulator(authGroupsPopulator);
}
private void setAuthenticator(final LdapAuthenticator authenticator) {
Assert.notNull(authenticator, "An LdapAuthenticator must be supplied");
this.authenticator = authenticator;
}
private LdapAuthenticator getAuthenticator() {
return authenticator;
}
private void setAuthoritiesPopulator(final LdapAuthoritiesPopulator authoritiesPopulator) {
Assert.notNull(authoritiesPopulator, "An LdapAuthoritiesPopulator must be supplied");
this.authoritiesPopulator = authoritiesPopulator;
}
protected LdapAuthoritiesPopulator getAuthoritiesPopulator() {
return authoritiesPopulator;
}
private void setAuthGroupsPopulator(final LdapAuthGroupsPopulator authGroupsPopulator) {
Assert.notNull(authGroupsPopulator, "An LdapAuthGroupsPopulator must be supplied");
this.authGroupsPopulator = authGroupsPopulator;
}
protected LdapAuthGroupsPopulator getAuthGroupsPopulator() {
return authGroupsPopulator;
}
/**
* Allows a custom strategy to be used for creating the <tt>UserDetails</tt> which will be stored as the principal
* in the <tt>Authentication</tt> returned by the
* {@link #createSuccessfulAuthentication(UsernamePasswordAuthenticationToken, UserDetails)} method.
*
* @param userDetailsContextMapper the strategy instance. If not set, defaults to a simple
* <tt>LdapUserDetailsMapper</tt>.
*/
public void setUserDetailsContextMapper(final CustomLdapUserDetailsMapper userDetailsContextMapper) {
Assert.notNull(userDetailsContextMapper, "UserDetailsContextMapper must not be null");
this.userDetailsContextMapper = userDetailsContextMapper;
}
/**
* Provides access to the injected <tt>UserDetailsContextMapper</tt> strategy for use by subclasses.
* @return CustomLdapUserDetailsMapper.
*/
protected CustomLdapUserDetailsMapper getUserDetailsContextMapper() {
return userDetailsContextMapper;
}
public void setHideUserNotFoundExceptions(final boolean hideUserNotFoundExceptions) {
this.hideUserNotFoundExceptions = hideUserNotFoundExceptions;
}
/**
* Determines whether the supplied password will be used as the credentials in the successful authentication
* token. If set to false, then the password will be obtained from the UserDetails object
* created by the configured <tt>UserDetailsContextMapper</tt>.
* Often it will not be possible to read the password from the directory, so defaults to true.
*
* @param useAuthenticationRequestCredentials true/false, as describes above
*/
public void setUseAuthenticationRequestCredentials(final boolean useAuthenticationRequestCredentials) {
this.useAuthenticationRequestCredentials = useAuthenticationRequestCredentials;
}
public void setMessageSource(final MessageSource messageSource) {
this.messages = new MessageSourceAccessor(messageSource);
}
/**
* This is the main method of this class, calling authentication, authorization and user details mapping.
* @param authentication object to populate
* @return Populated authentication object
* @throws AuthenticationException
*/
public Authentication authenticate(final Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
logger.finest("CustomLdapAuthenticationProvider: authenticate");
final UsernamePasswordAuthenticationToken userToken = (UsernamePasswordAuthenticationToken) authentication;
String username = userToken.getName();
String password = (String) authentication.getCredentials();
logger.fine("Processing authentication request for user: " + username);
if (!StringUtils.hasLength(username)) {
throw new BadCredentialsException(messages.getMessage("LdapAuthenticationProvider.emptyUsername",
"Empty Username"));
}
Assert.notNull(password, "Null password was supplied in authentication token");
try {
DirContextOperations userData = getAuthenticator().authenticate(authentication);
Collection<? extends GrantedAuthority> extraAuthorities = loadUserAuthorities(userData, username, password);
Collection<String> userAuthGroups = loadUserAuthGroups(userData, username, password);
ExtendedLdapUserDetailsImpl extendedUserDetails =
userDetailsContextMapper.mapUserFromContext(userData, username, extraAuthorities, userAuthGroups);
return createSuccessfulAuthentication(userToken, extendedUserDetails);
} catch (PasswordPolicyException ppe) {
// The only reason a policy exception can occur during a bind is that the account is locked.
throw new LockedException(messages.getMessage(ppe.getStatus().getErrorCode(),
ppe.getStatus().getDefaultMessage()));
} catch (UsernameNotFoundException notFound) {
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"LdapAuthenticationProvider.badCredentials", "Bad credentials"));
} else {
throw notFound;
}
} catch (NamingException ldapAccessFailure) {
throw new AuthenticationServiceException(ldapAccessFailure.getMessage(), ldapAccessFailure);
}
}
/**
* loads the user's roles (authorities).
* @param user .
* @param username .
* @param password .
* @return Collections of {@link GrantedAuthority} objects
*/
protected Collection<? extends GrantedAuthority> loadUserAuthorities(final DirContextOperations user, final String username,
final String password) {
return getAuthoritiesPopulator().getGrantedAuthorities(user, username);
}
/**
* loads the user's authorization groups.
* @param user .
* @param username .
* @param password .
* @return Collection of authorization groups names.
*/
protected Collection<String> loadUserAuthGroups(final DirContextOperations user, final String username,
final String password) {
return getAuthGroupsPopulator().getAuthGroups(user, username);
}
/**
* Creates the final <tt>Authentication</tt> object which will be returned from the <tt>authenticate</tt> method.
*
* @param authentication the original authentication request token
* @param user the <tt>UserDetails</tt> instance returned by the configured <tt>UserDetailsContextMapper</tt>.
* @return the Authentication object for the fully authenticated user.
*/
protected Authentication createSuccessfulAuthentication(final UsernamePasswordAuthenticationToken authentication,
final ExtendedLdapUserDetailsImpl user) {
logger.finest("CustomLdapAuthenticationProvider : createSuccessfulAuthentication");
Object password = useAuthenticationRequestCredentials ? authentication.getCredentials() : user.getPassword();
CustomAuthenticationToken customAuthToken = new CustomAuthenticationToken(user, password,
user.getAuthorities(), user.getAuthGroups());
customAuthToken.setDetails(authentication.getDetails());
return customAuthToken;
}
/**
* Checks if the authentication object passed is supported.
* @param authentication The authentication object to check.
* @return true - supported, false - otherwise.
*/
public boolean supports(final Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
}