package net.unicon.cas.addons.authentication.strong.yubikey; import com.yubico.client.v2.YubicoClient; import com.yubico.client.v2.YubicoResponse; import com.yubico.client.v2.YubicoResponseStatus; import org.jasig.cas.authentication.handler.AuthenticationException; import org.jasig.cas.authentication.handler.BadUsernameOrPasswordAuthenticationException; import org.jasig.cas.authentication.handler.support.AbstractUsernamePasswordAuthenticationHandler; import org.jasig.cas.authentication.principal.UsernamePasswordCredentials; import org.springframework.beans.factory.InitializingBean; /** * An authentication handler that uses the Yubico cloud validation platform to authenticate * one-time password tokens that are issued by a YubiKey device. To use YubiCloud you need a * client id and an API key which must be obtained from Yubico. * <p/> * <p>For more info, please visit <a href="http://yubico.github.io/yubico-java-client/">this link</a></p> * * @author Misagh Moayyed mmoayyed@unicon.net * @since 1.5 */ public class YubiKeyAuthenticationHandler extends AbstractUsernamePasswordAuthenticationHandler implements InitializingBean { private YubiKeyAccountRegistry registry = new AcceptAnyYubiKeyAccountRegistry(); private YubicoClient client; /** * Prepares the Yubico client with the received clientId and secretKey. By default, * all YubiKey accounts are allowed to authenticate. * <p/> * WARNING: THIS CONSTRUCTOR RESULTS IN AN EXAMPLE YubiKeyAuthenticationHandler * CONFIGURATION THAT CONSIDERS ALL Yubikeys VALID FOR ALL USERS. YOU MUST NOT USE * THIS CONSTRUCTOR IN PRODUCTION. * * @param clientId * @param secretKey */ public YubiKeyAuthenticationHandler(final Integer clientId, final String secretKey) { this.client = YubicoClient.getClient(clientId); this.client.setKey(secretKey); } /** * Prepares the Yubico client with the received clientId and secretKey. If you wish to * limit the usage of this handler only to a particular set of yubikey accounts for a special * group of users, you may provide an compliant implementation of {@link YubiKeyAccountRegistry}. * By default, all accounts are allowed. * * @param clientId * @param secretKey * @param registry */ public YubiKeyAuthenticationHandler(final Integer clientId, final String secretKey, final YubiKeyAccountRegistry registry) { this(clientId, secretKey); this.registry = registry; } @Override public void afterPropertiesSet() throws Exception { if (this.registry instanceof AcceptAnyYubiKeyAccountRegistry) { log.warn("{} instantiated with example accept-any configuration handled via {}. " + "THIS IS NOT OKAY IN PRODUCTION. NO. NO. NO.", this.getClass().getSimpleName(), AcceptAnyYubiKeyAccountRegistry.class.getSimpleName()); } } /** * {@inheritDoc} * <p/> * Attempts to authenticate the received credentials using the Yubico cloud validation platform. * In this implementation, the {@link org.jasig.cas.authentication.principal.UsernamePasswordCredentials#getUsername()} * is mapped to the <code>uid</code> which will be used by the plugged-in instance of the {@link YubiKeyAccountRegistry} * and the {@link org.jasig.cas.authentication.principal.UsernamePasswordCredentials#getPassword()} is the received * one-time password token issued sby the YubiKey device. * * @param usernamePasswordCredentials * * @return true if the authentication succeeds. False, otherwise. * * @throws AuthenticationException */ @Override protected boolean authenticateUsernamePasswordInternal(final UsernamePasswordCredentials usernamePasswordCredentials) throws AuthenticationException { try { final String uid = usernamePasswordCredentials.getUsername(); final String otp = usernamePasswordCredentials.getPassword(); if (YubicoClient.isValidOTPFormat(otp)) { final String publicId = YubicoClient.getPublicId(otp); if (this.registry.isYubiKeyRegisteredFor(uid, publicId)) { final YubicoResponse response = client.verify(otp); log.debug("YubiKey response status {} at {}", response.getStatus(), response.getTimestamp()); return (response.getStatus() == YubicoResponseStatus.OK); } else { log.debug("YubiKey public id [{}] is not registered for user [{}]", publicId, uid); } } else { log.debug("Invalid OTP format [{}]", otp); } return false; } catch (final Exception e) { throw new BadUsernameOrPasswordAuthenticationException(e); } } /** * Example implementation of YubiKeyAccountRegistry that considers all yubikey Ids * registered for all users. * THIS IS OBVIOUSLY COMPLETELY UNACCEPTABLE FOR PRODUCTION USE AND YOU MUST USE * A REGISTRY THAT ACTUALLY REGISTERS IN PRODUCTION. */ private static final class AcceptAnyYubiKeyAccountRegistry implements YubiKeyAccountRegistry { @Override public boolean isYubiKeyRegisteredFor(final String uid, final String yubikeyPublicId) { return true; } } }