Injecting Custom Authentication
You may recall from my first post in this series that my goal was to use Spring Security to enforce user authentication and authorization through my page while using my custom project Passport. Within Passport, I’ve built user management and an email confirmation component.
The first hurdle to overcome was integrating Spring Security out of the box into my web app. That was solved on part 1. Now, the more interesting challenge of pushing Passport’s functionality into Spring Security.
1. Implement an AuthenticationProvider
The AuthenticationProvider interface in Spring Security is needed by the AuthenticationManager to process authenticating users. So I wrote my own implementation of this:
package ben.kn.passport; import java.util.Collection; import java.util.HashSet; import org.apache.log4j.Logger; import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.ProviderNotFoundException; 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.userdetails.UsernameNotFoundException; import ben.kn.passport.lang.PassportException; import ben.kn.passport.to.User; import ben.kn.passport.to.UserRole; /** * The {@link AuthenticationProvider} for using Passport to oversee user * authentication. * * @author Ben ([email protected]) * @since Feb 1, 2013 */ public class PassportAuthenticationProvider implements AuthenticationProvider { protected final Logger log = Logger.getLogger(getClass()); private UserService userSvc; private CipherService cipherService; public PassportAuthenticationProvider(UserService userSvc, CipherService cipherService) { this.userSvc = userSvc; this.cipherService = cipherService; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { // check that the authentication has actually been provided if (authentication == null) { log.debug("Attempted to authenticate but the given authentication was null."); throw new AuthenticationCredentialsNotFoundException("No authentication provided!"); } if (!(authentication instanceof UsernamePasswordAuthenticationToken)) { log.warn("Attempted to authenticate but the given authentication was not the kind expected. Instead it was " + authentication.getClass()); throw new BadCredentialsException( "The wrong type of authorization has been given, cannot continue."); } // get username and password String username = (authentication.getPrincipal() == null) ? null : authentication.getName(); String password = (String) ((authentication.getPrincipal() == null) ? null : authentication .getCredentials()); User user = null; // verify the username was found if (username != null) { try { // get the User with this username user = userSvc.selectByName(username); if (user != null) { // check the password if (password != null) { String encryptedPassword = cipherService.encryptPassword(password); // if it doesn't match, then throw an exception if (!encryptedPassword.equals(user.getPassword())) { throw new BadCredentialsException("Invalid password provided."); } } } else { throw new UsernameNotFoundException( "The user you've attempted to log in as was not found."); } } catch (PassportException e) { throw new ProviderNotFoundException(e.getMessage()); } } else { throw new UsernameNotFoundException("No username was given in the login attempt."); } return createSuccessAuthentication(authentication, user); } /** * Create a successful {@link Authentication} object. * * @param authentication Authentication original that was given to the * provider * @param user User that was loaded by the implementation * @return Authentication */ private Authentication createSuccessAuthentication(Authentication authentication, User user) { // use the role set on the User UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(user, user.getPassword(), toCol(user.getRole())); result.setDetails(authentication.getDetails()); return result; } /** * Since users currently only have one {@link UserRole}, convert that into a * Collection. * * @param role UserRole * @return Collection of UserRoles */ private Collection<? extends GrantedAuthority> toCol(UserRole role) { Collection<UserRole> col = new HashSet<UserRole>(); col.add(role); return col; } @Override public boolean supports(Class<?> authentication) { // check if the authentication is assignable to the desire token type // (UsernamePasswordAuthenticationToken) return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication)); } }
To quickly review the code:
- – The constructor instantiates the UserService for retrieving users, and the CipherService for executing encryption/decryption.
- – The authenticate</span function implements the good stuff for authenticating the user with the Passport UserService. It throws a lot of different kinds of exceptions based on any problems found.
- – The private createSuccessAuthentication function creates the revised Authentication object.
- – The toCol function is creating a single element Collection for the Authentication’s authorizations.
- – The supports function tells the AuthenticationManager whether the given Authentication request is supported with this Provider.
Now there are some other pieces necessary for this to be successful.
2. Create Roles Implementing GrantedAuthority
Spring Security is expecting implementations of the GrantedAuthority interface to represent the roles used in the various annotations and security tags. To do this, I created an enum:
package ben.kn.passport.to; import org.springframework.security.core.GrantedAuthority; /** * Enum represents a role for a {@link User} * * @author Ben ([email protected]) * @since Jan 29, 2013 */ public enum UserRole implements GrantedAuthority { ADMIN("Administrator"), USER("User"); private String display; UserRole(String display) { this.display = display; } public String getDisplay() { return display; } public static UserRole fromBytes(byte[] bytes) { if (bytes != null) { UserRole.valueOf(new String(bytes)); } return null; } @Override public String getAuthority() { return toString(); } }
Pretty straightforward, implement the getAuthority() function to return the text representation of the role. Another option for this (which I will likely implement later) is to have this as an object stored in the database.
3. The User Object Implementing Principal
The User object which I’m using must implement Principal, embodying the properties of a security entity. The only function requiring an override is getName().
4. Tell AuthenticationManager to Use It!
Now return to your security-context.xml, and update the <security:authentication-manager> to use our AuthenticationProvider:
<bean id="passportAuthenticationProvider" class="ben.kn.passport.PassportAuthenticationProvider"> <constructor-arg index="0" ref="userService" /> <constructor-arg index="1" ref="cipherService" /> </bean><security:authentication-manager> <security:authentication-provider ref="passportAuthenticationProvider"></security:authentication-provider> </security:authentication-manager>
This constructs the PassportAuthenticationProvider and injects it into the manager. I’d also like to point out that since I’m using a Java-based configuration in Spring (a class with the @Configuration annotation), I can set these bean references without declaring them within the XML file. Spring will construct the beans and put them in for me.