How to customize a spring-security form-login. Part 3

An article from my zk integration series.

part 1
part 2
part 4
part 5

AbstractUserDetailsAuthenticationProvider.java
The method additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordTenantAuthenticationToken authentication) allow us additional checks for a returned UserDetails. We adapt it only to our UsernamePasswordTenantAuthenticationToken.

AbstractUserDetailsAuthenticationProvider.java

public abstract class MyAbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
	// ~ Instance fields
	// ================================================================================================

	protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
	private UserCache userCache = new NullUserCache();
	private boolean forcePrincipalAsString = false;
	protected boolean hideUserNotFoundExceptions = true;
	private UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks();
	private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks();

	// ~ Methods
	// ========================================================================================================

	/**
	 * Allows subclasses to perform any additional checks of a returned (or
	 * cached) <code>UserDetails</code> for a given authentication request.
	 * Generally a subclass will at least compare the
	 * {@link Authentication#getCredentials()} with a
	 * {@link UserDetails#getPassword()}. If custom logic is needed to compare
	 * additional properties of <code>UserDetails</code> and/or
	 * <code>UsernamePasswordAuthenticationToken</code>, these should also
	 * appear in this method.
	 * 
	 * @param userDetails
	 *            as retrieved from the
	 *            {@link #retrieveUser(String, UsernamePasswordAuthenticationToken)}
	 *            or <code>UserCache</code>
	 * @param authentication
	 *            the current request that needs to be authenticated
	 * 
	 * @throws AuthenticationException
	 *             AuthenticationException if the credentials could not be
	 *             validated (generally a <code>BadCredentialsException</code>,
	 *             an <code>AuthenticationServiceException</code>)
	 */
	protected abstract void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordTenantAuthenticationToken authentication) throws AuthenticationException;

	public final void afterPropertiesSet() throws Exception {
		Assert.notNull(this.userCache, "A user cache must be set");
		Assert.notNull(this.messages, "A message source must be set");
		doAfterPropertiesSet();
	}

	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		Assert.isInstanceOf(UsernamePasswordTenantAuthenticationToken.class, authentication, messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
				"Only UsernamePasswordTenantAuthenticationToken is supported"));

		// Determine username
		String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();

		boolean cacheWasUsed = true;
		UserDetails user = this.userCache.getUserFromCache(username);

		if (user == null) {
			cacheWasUsed = false;

			try {
				user = retrieveUser(username, (UsernamePasswordTenantAuthenticationToken) authentication);
			} catch (UsernameNotFoundException notFound) {
				if (hideUserNotFoundExceptions) {
					throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
				} else {
					throw notFound;
				}
			}

			Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
		}

		try {
			preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user, (UsernamePasswordTenantAuthenticationToken) authentication);
		} catch (AuthenticationException exception) {
			if (cacheWasUsed) {
				// There was a problem, so try again after checking
				// we're using latest data (i.e. not from the cache)
				cacheWasUsed = false;
				user = retrieveUser(username, (UsernamePasswordTenantAuthenticationToken) authentication);
				preAuthenticationChecks.check(user);
				additionalAuthenticationChecks(user, (UsernamePasswordTenantAuthenticationToken) authentication);
			} else {
				throw exception;
			}
		}

		postAuthenticationChecks.check(user);

		if (!cacheWasUsed) {
			this.userCache.putUserInCache(user);
		}

		Object principalToReturn = user;

		if (forcePrincipalAsString) {
			principalToReturn = user.getUsername();
		}

		return createSuccessAuthentication(principalToReturn, authentication, user);
	}

	/**
	 * Creates a successful {@link Authentication} object.
	 * <p>
	 * Protected so subclasses can override.
	 * </p>
	 * <p>
	 * Subclasses will usually store the original credentials the user supplied
	 * (not salted or encoded passwords) in the returned
	 * <code>Authentication</code> object.
	 * </p>
	 * 
	 * @param principal
	 *            that should be the principal in the returned object (defined
	 *            by the {@link #isForcePrincipalAsString()} method)
	 * @param authentication
	 *            that was presented to the provider for validation
	 * @param user
	 *            that was loaded by the implementation
	 * 
	 * @return the successful authentication token
	 */
	protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
		// Ensure we return the original credentials the user supplied,
		// so subsequent attempts are successful even with encoded passwords.
		// Also ensure we return the original getDetails(), so that future
		// authentication events after cache expiry contain the details

		// OLD
		// UsernamePasswordTenantAuthenticationToken result = new
		// UsernamePasswordTenantAuthenticationToken(principal,
		// authentication.getCredentials(), user.getAuthorities());
		UsernamePasswordTenantAuthenticationToken result = new UsernamePasswordTenantAuthenticationToken(principal, authentication.getCredentials(), user.getAuthorities(),
				((UsernamePasswordTenantAuthenticationToken) authentication).getTenantId());
		result.setDetails(authentication.getDetails());

		return result;
	}

	protected void doAfterPropertiesSet() throws Exception {
	}

	public UserCache getUserCache() {
		return userCache;
	}

	public boolean isForcePrincipalAsString() {
		return forcePrincipalAsString;
	}

	public boolean isHideUserNotFoundExceptions() {
		return hideUserNotFoundExceptions;
	}

	/**
	 * Allows subclasses to actually retrieve the <code>UserDetails</code> from
	 * an implementation-specific location, with the option of throwing an
	 * <code>AuthenticationException</code> immediately if the presented
	 * credentials are incorrect (this is especially useful if it is necessary
	 * to bind to a resource as the user in order to obtain or generate a
	 * <code>UserDetails</code>).
	 * <p>
	 * Subclasses are not required to perform any caching, as the
	 * <code>AbstractUserDetailsAuthenticationProvider</code> will by default
	 * cache the <code>UserDetails</code>. The caching of
	 * <code>UserDetails</code> does present additional complexity as this means
	 * subsequent requests that rely on the cache will need to still have their
	 * credentials validated, even if the correctness of credentials was assured
	 * by subclasses adopting a binding-based strategy in this method.
	 * Accordingly it is important that subclasses either disable caching (if
	 * they want to ensure that this method is the only method that is capable
	 * of authenticating a request, as no <code>UserDetails</code> will ever be
	 * cached) or ensure subclasses implement
	 * {@link #additionalAuthenticationChecks(UserDetails, UsernamePasswordAuthenticationToken)}
	 * to compare the credentials of a cached <code>UserDetails</code> with
	 * subsequent authentication requests.
	 * </p>
	 * <p>
	 * Most of the time subclasses will not perform credentials inspection in
	 * this method, instead performing it in
	 * {@link #additionalAuthenticationChecks(UserDetails, UsernamePasswordAuthenticationToken)}
	 * so that code related to credentials validation need not be duplicated
	 * across two methods.
	 * </p>
	 * 
	 * @param username
	 *            The username to retrieve
	 * @param authentication
	 *            The authentication request, which subclasses <em>may</em> need
	 *            to perform a binding-based retrieval of the
	 *            <code>UserDetails</code>
	 * 
	 * @return the user information (never <code>null</code> - instead an
	 *         exception should the thrown)
	 * 
	 * @throws AuthenticationException
	 *             if the credentials could not be validated (generally a
	 *             <code>BadCredentialsException</code>, an
	 *             <code>AuthenticationServiceException</code> or
	 *             <code>UsernameNotFoundException</code>)
	 */
	protected abstract UserDetails retrieveUser(String username, UsernamePasswordTenantAuthenticationToken authentication) throws AuthenticationException;

	public void setForcePrincipalAsString(boolean forcePrincipalAsString) {
		this.forcePrincipalAsString = forcePrincipalAsString;
	}

	/**
	 * By default the <code>AbstractUserDetailsAuthenticationProvider</code>
	 * throws a <code>BadCredentialsException</code> if a username is not found
	 * or the password is incorrect. Setting this property to <code>false</code>
	 * will cause <code>UsernameNotFoundException</code>s to be thrown instead
	 * for the former. Note this is considered less secure than throwing
	 * <code>BadCredentialsException</code> for both exceptions.
	 * 
	 * @param hideUserNotFoundExceptions
	 *            set to <code>false</code> if you wish
	 *            <code>UsernameNotFoundException</code>s to be thrown instead
	 *            of the non-specific <code>BadCredentialsException</code>
	 *            (defaults to <code>true</code>)
	 */
	public void setHideUserNotFoundExceptions(boolean hideUserNotFoundExceptions) {
		this.hideUserNotFoundExceptions = hideUserNotFoundExceptions;
	}

	public void setMessageSource(MessageSource messageSource) {
		this.messages = new MessageSourceAccessor(messageSource);
	}

	public void setUserCache(UserCache userCache) {
		this.userCache = userCache;
	}

	public boolean supports(Class<? extends Object> authentication) {
		return (UsernamePasswordTenantAuthenticationToken.class.isAssignableFrom(authentication));
	}

	protected UserDetailsChecker getPreAuthenticationChecks() {
		return preAuthenticationChecks;
	}

	/**
	 * Sets the policy will be used to verify the status of the loaded
	 * <tt>UserDetails</tt> <em>before</em> validation of the credentials takes
	 * place.
	 * 
	 * @param preAuthenticationChecks
	 *            strategy to be invoked prior to authentication.
	 */
	public void setPreAuthenticationChecks(UserDetailsChecker preAuthenticationChecks) {
		this.preAuthenticationChecks = preAuthenticationChecks;
	}

	protected UserDetailsChecker getPostAuthenticationChecks() {
		return postAuthenticationChecks;
	}

	public void setPostAuthenticationChecks(UserDetailsChecker postAuthenticationChecks) {
		this.postAuthenticationChecks = postAuthenticationChecks;
	}

	private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
		public void check(UserDetails user) {
			if (!user.isAccountNonLocked()) {
				throw new LockedException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"), user);
			}

			if (!user.isEnabled()) {
				throw new DisabledException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"), user);
			}

			if (!user.isAccountNonExpired()) {
				throw new AccountExpiredException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"), user);
			}
		}
	}

	private class DefaultPostAuthenticationChecks implements UserDetailsChecker {
		public void check(UserDetails user) {
			if (!user.isCredentialsNonExpired()) {
				throw new CredentialsExpiredException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.credentialsExpired", "User credentials have expired"), user);
			}
		}
	}
}

MyDaoAuthenticationProvider.java
An implementation of an AuthenticationProvider that retrieves the user details from an UserDetailsService.

MyDaoAuthenticationProvider .java

public class MyDaoAuthenticationProvider extends MyAbstractUserDetailsAuthenticationProvider {

	// ~ Instance fields
	// ================================================================================================

	private PasswordEncoder passwordEncoder = new PlaintextPasswordEncoder();

	private SaltSource saltSource;

	private UserDetailsService userDetailsService;

	private boolean includeDetailsObject = true;

	// ~ Methods
	// ========================================================================================================

	protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordTenantAuthenticationToken authentication) throws AuthenticationException {
		Object salt = null;

		if (this.saltSource != null) {
			salt = this.saltSource.getSalt(userDetails);
		}

		if (authentication.getCredentials() == null) {
			throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"), includeDetailsObject ? userDetails : null);
		}

		String presentedPassword = authentication.getCredentials().toString();

		if (!passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, salt)) {
			throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"), includeDetailsObject ? userDetails : null);
		}
	}

	protected void doAfterPropertiesSet() throws Exception {
		Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
	}

	protected final UserDetails retrieveUser(String username, UsernamePasswordTenantAuthenticationToken authentication) throws AuthenticationException {
		UserDetails loadedUser;

		try {
			loadedUser = this.getUserDetailsService().loadUserByUsername(username + "/" + authentication.getTenantId());
		} catch (DataAccessException repositoryProblem) {
			throw new AuthenticationServiceException(repositoryProblem.getMessage(), repositoryProblem);
		}

		if (loadedUser == null) {
			throw new AuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
		}
		return loadedUser;
	}

	/**
	 * Sets the PasswordEncoder instance to be used to encode and validate
	 * passwords. If not set, {@link PlaintextPasswordEncoder} will be used by
	 * default.
	 * 
	 * @param passwordEncoder
	 *            The passwordEncoder to use
	 */
	public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
		this.passwordEncoder = passwordEncoder;
	}

	protected PasswordEncoder getPasswordEncoder() {
		return passwordEncoder;
	}

	/**
	 * The source of salts to use when decoding passwords. <code>null</code> is
	 * a valid value, meaning the <code>DaoAuthenticationProvider</code> will
	 * present <code>null</code> to the relevant <code>PasswordEncoder</code>.
	 * 
	 * @param saltSource
	 *            to use when attempting to decode passwords via the
	 *            <code>PasswordEncoder</code>
	 */
	public void setSaltSource(SaltSource saltSource) {
		this.saltSource = saltSource;
	}

	protected SaltSource getSaltSource() {
		return saltSource;
	}

	public void setUserDetailsService(UserDetailsService userDetailsService) {
		this.userDetailsService = userDetailsService;
	}

	protected UserDetailsService getUserDetailsService() {
		return userDetailsService;
	}

	protected boolean isIncludeDetailsObject() {
		return includeDetailsObject;
	}

	/**
	 * Determines whether the UserDetails will be included in the
	 * <tt>extraInformation</tt> field of a thrown BadCredentialsException.
	 * Defaults to true, but can be set to false if the exception will be used
	 * with a remoting protocol, for example.
	 * 
	 * @deprecated use
	 *             {@link org.springframework.security.authentication.ProviderManager#setClearExtraInformation(boolean)}
	 */
	public void setIncludeDetailsObject(boolean includeDetailsObject) {
		this.includeDetailsObject = includeDetailsObject;
	}
}

Part 4 of this article you can read here.

Samples are hostet in the Zksample2 project on

Have fun with it.

Stephan Gerth

Dipl.rer.pol.


PS: Help to prevent the global warming by writing cool software

Leave a Reply

You must be logged in to post a comment.