Skip to content

Instantly share code, notes, and snippets.

@mmaravich
Created August 29, 2014 09:22
Show Gist options
  • Save mmaravich/d71c60f0c5b4887f598c to your computer and use it in GitHub Desktop.
Save mmaravich/d71c60f0c5b4887f598c to your computer and use it in GitHub Desktop.
Spring Security authentication provider that uses YubiKey OTP as password
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