Created
August 29, 2014 09:22
-
-
Save mmaravich/d71c60f0c5b4887f598c to your computer and use it in GitHub Desktop.
Spring Security authentication provider that uses YubiKey OTP as password
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.curlapp.yubikey; | |
import org.slf4j.Logger; | |
import org.slf4j.LoggerFactory; | |
import org.springframework.beans.factory.InitializingBean; | |
import org.springframework.security.authentication.AccountExpiredException; | |
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.InternalAuthenticationServiceException; | |
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.authority.mapping.GrantedAuthoritiesMapper; | |
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper; | |
import org.springframework.security.core.userdetails.UserDetails; | |
import org.springframework.security.core.userdetails.UserDetailsChecker; | |
import org.springframework.security.core.userdetails.UserDetailsService; | |
import org.springframework.security.core.userdetails.UsernameNotFoundException; | |
import org.springframework.util.Assert; | |
import com.yubico.client.v2.YubicoClient; | |
import com.yubico.client.v2.YubicoResponse; | |
import com.yubico.client.v2.YubicoResponseStatus; | |
import com.yubico.client.v2.exceptions.YubicoValidationException; | |
import com.yubico.client.v2.exceptions.YubicoValidationFailure; | |
public class YubikeyAuthenticationProvider implements AuthenticationProvider, InitializingBean { | |
private static final Logger LOG = LoggerFactory.getLogger(YubikeyAuthenticationProvider.class); | |
private UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks(); | |
private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks(); | |
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper(); | |
private UserDetailsService userDetailsService; | |
private Integer yubicoClientId; | |
public final void afterPropertiesSet() throws Exception { | |
Assert.notNull(this.preAuthenticationChecks, "preAuthenticationChecks must be set"); | |
Assert.notNull(this.postAuthenticationChecks, "postAuthenticationChecks must be set"); | |
Assert.notNull(this.authoritiesMapper, "authoritiesMapper must be set"); | |
Assert.notNull(this.userDetailsService, "A UserDetailsService must be set"); | |
Assert.notNull(this.yubicoClientId, "Yubico Client ID must be set"); | |
} | |
@Override | |
public Authentication authenticate(Authentication authentication) throws AuthenticationException { | |
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, "Only UsernamePasswordAuthenticationToken is supported"); | |
// Determine username | |
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName(); | |
UserDetails user = null; | |
try { | |
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); | |
} catch (UsernameNotFoundException notFound) { | |
LOG.debug("User '" + username + "' not found"); | |
throw new BadCredentialsException("Bad credentials"); | |
} | |
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract"); | |
try { | |
preAuthenticationChecks.check(user); | |
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); | |
} catch (AuthenticationException exception) { | |
throw exception; | |
} | |
postAuthenticationChecks.check(user); | |
Object principalToReturn = user; | |
return createSuccessAuthentication(principalToReturn, authentication, user); | |
} | |
@Override | |
public boolean supports(Class<?> authentication) { | |
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication)); | |
} | |
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { | |
String presentedPassword = authentication.getCredentials().toString(); | |
YubicoClient client = YubicoClient.getClient(this.yubicoClientId); | |
YubicoResponse response; | |
try { | |
response = client.verify(presentedPassword); | |
} catch (YubicoValidationException e) { | |
LOG.debug("Authentication failed: YubicoValidationException", e); | |
throw new BadCredentialsException("Could not verify credentials"); | |
} catch (YubicoValidationFailure e) { | |
LOG.debug("Authentication failed: YubicoValidationFailure", e); | |
throw new BadCredentialsException("Could not verify credentials"); | |
} | |
if (response.getStatus() != YubicoResponseStatus.OK) { | |
LOG.debug("Authentication failed: password does not match stored value"); | |
throw new BadCredentialsException("Bad credentials"); | |
} | |
YubikeyUserDetails yubiuser = (YubikeyUserDetails) userDetails; | |
if (!YubicoClient.getPublicId(presentedPassword).equals(yubiuser.getPublicId())) { | |
LOG.debug("Authentication failed: publicId does not match stored value"); | |
throw new BadCredentialsException("Bad credentials"); | |
} | |
} | |
protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { | |
UserDetails loadedUser; | |
try { | |
loadedUser = this.userDetailsService.loadUserByUsername(username); | |
if (loadedUser == null) { | |
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation"); | |
} | |
if (!(loadedUser instanceof YubikeyUserDetails)) { | |
throw new InternalAuthenticationServiceException( | |
"UserDetailsService returned an object that is not an instance of YubikeyUserDetails, which is an interface contract violation"); | |
} | |
} catch (UsernameNotFoundException notFound) { | |
throw notFound; | |
} catch (Exception repositoryProblem) { | |
throw new InternalAuthenticationServiceException(repositoryProblem.getMessage(), repositoryProblem); | |
} | |
return loadedUser; | |
} | |
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 | |
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(), | |
authoritiesMapper.mapAuthorities(user.getAuthorities())); | |
result.setDetails(authentication.getDetails()); | |
return result; | |
} | |
private class DefaultPreAuthenticationChecks implements UserDetailsChecker { | |
public void check(UserDetails user) { | |
if (!user.isAccountNonLocked()) { | |
LOG.debug("User account is locked"); | |
throw new LockedException("User account is locked"); | |
} | |
if (!user.isEnabled()) { | |
LOG.debug("User account is disabled"); | |
throw new DisabledException("User is disabled"); | |
} | |
if (!user.isAccountNonExpired()) { | |
LOG.debug("User account is expired"); | |
throw new AccountExpiredException("User account has expired"); | |
} | |
} | |
} | |
private class DefaultPostAuthenticationChecks implements UserDetailsChecker { | |
public void check(UserDetails user) { | |
if (!user.isCredentialsNonExpired()) { | |
LOG.debug("User account credentials have expired"); | |
throw new CredentialsExpiredException("User credentials have expired"); | |
} | |
} | |
} | |
public UserDetailsChecker getPreAuthenticationChecks() { | |
return preAuthenticationChecks; | |
} | |
public void setPreAuthenticationChecks(UserDetailsChecker preAuthenticationChecks) { | |
this.preAuthenticationChecks = preAuthenticationChecks; | |
} | |
public UserDetailsChecker getPostAuthenticationChecks() { | |
return postAuthenticationChecks; | |
} | |
public void setPostAuthenticationChecks(UserDetailsChecker postAuthenticationChecks) { | |
this.postAuthenticationChecks = postAuthenticationChecks; | |
} | |
public GrantedAuthoritiesMapper getAuthoritiesMapper() { | |
return authoritiesMapper; | |
} | |
public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) { | |
this.authoritiesMapper = authoritiesMapper; | |
} | |
public UserDetailsService getUserDetailsService() { | |
return userDetailsService; | |
} | |
public void setUserDetailsService(UserDetailsService userDetailsService) { | |
this.userDetailsService = userDetailsService; | |
} | |
public Integer getYubicoClientId() { | |
return yubicoClientId; | |
} | |
public void setYubicoClientId(Integer yubicoClientId) { | |
this.yubicoClientId = yubicoClientId; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment