Introduction to YubiKey Authentication
YubiKey is a hardware authentication device that provides strong two-factor authentication (2FA) and multi-factor authentication (MFA) for applications. Developed by Yubico, these USB and NFC devices generate one-time passwords (OTP) and support modern authentication protocols like FIDO2/WebAuthn.
YubiKey Authentication Methods
1. YubiKey OTP Validation
The traditional method using one-time passwords.
2. FIDO2/WebAuthn
Modern passwordless authentication standard.
3. PIV (Personal Identity Verification)
Smart card-based authentication.
Setting Up Dependencies
Maven Configuration
<dependencies> <!-- Yubico OTP Validation --> <dependency> <groupId>com.yubico</groupId> <artifactId>yubico-validator</artifactId> <version>1.6.0</version> </dependency> <!-- FIDO2/WebAuthn --> <dependency> <groupId>com.yubico</groupId> <artifactId>webauthn-server-core</artifactId> <version>1.12.0</version> </dependency> <!-- YubiKit for Android --> <dependency> <groupId>com.yubico.yubikit</groupId> <artifactId>yubikit</artifactId> <version>2.3.0</version> </dependency> </dependencies>
Gradle Configuration
dependencies {
implementation 'com.yubico:yubico-validator:1.6.0'
implementation 'com.yubico:webauthn-server-core:1.12.0'
implementation 'com.yubico.yubikit:yubikit:2.3.0'
}
YubiKey OTP Integration
Basic OTP Validation
import com.yubico.client.v2.YubicoClient;
import com.yubico.client.v2.VerificationResponse;
import com.yubico.client.v2.exceptions.YubicoValidationFailure;
import com.yubico.client.v2.exceptions.YubicoVerificationException;
public class YubiKeyOTPValidator {
private final YubicoClient yubicoClient;
public YubiKeyOTPValidator(String clientId, String secretKey) {
this.yubicoClient = YubicoClient.getClient(clientId, secretKey);
}
public boolean validateOTP(String otp) {
try {
VerificationResponse response = yubicoClient.verify(otp);
return response.isOk();
} catch (YubicoVerificationException | YubicoValidationFailure e) {
System.err.println("YubiKey validation failed: " + e.getMessage());
return false;
}
}
public String extractPublicId(String otp) {
return YubicoClient.getPublicId(otp);
}
}
Advanced OTP Validation with Custom Configuration
import com.yubico.client.v2.*;
import com.yubico.client.v2.exceptions.*;
import java.util.Arrays;
import java.util.List;
public class AdvancedYubiKeyValidator {
private YubicoClient yubicoClient;
private List<String> allowedYubiKeyIds;
public AdvancedYubiKeyValidator(String clientId, String secretKey,
List<String> allowedYubiKeyIds) {
this.allowedYubiKeyIds = allowedYubiKeyIds;
this.yubicoClient = YubicoClient.getClient(clientId, secretKey);
this.yubicoClient.setSyncApiUrls(Arrays.asList(
"https://api.yubico.com/wsapi/2.0/verify",
"https://api2.yubico.com/wsapi/2.0/verify",
"https://api3.yubico.com/wsapi/2.0/verify",
"https://api4.yubico.com/wsapi/2.0/verify",
"https://api5.yubico.com/wsapi/2.0/verify"
));
}
public ValidationResult validateYubiKey(String otp) {
try {
VerificationResponse response = yubicoClient.verify(otp);
if (!response.isOk()) {
return new ValidationResult(false, "OTP validation failed: " + response.getStatus());
}
String publicId = YubicoClient.getPublicId(otp);
if (!isYubiKeyAllowed(publicId)) {
return new ValidationResult(false, "YubiKey not authorized: " + publicId);
}
return new ValidationResult(true, "Validation successful", publicId);
} catch (Exception e) {
return new ValidationResult(false, "Validation error: " + e.getMessage());
}
}
private boolean isYubiKeyAllowed(String publicId) {
return allowedYubiKeyIds.contains(publicId);
}
public static class ValidationResult {
private final boolean valid;
private final String message;
private final String yubiKeyId;
public ValidationResult(boolean valid, String message) {
this(valid, message, null);
}
public ValidationResult(boolean valid, String message, String yubiKeyId) {
this.valid = valid;
this.message = message;
this.yubiKeyId = yubiKeyId;
}
// Getters
public boolean isValid() { return valid; }
public String getMessage() { return message; }
public String getYubiKeyId() { return yubiKeyId; }
}
}
FIDO2/WebAuthn Integration
WebAuthn Registration
import com.yubico.webauthn.*;
import com.yubico.webauthn.data.*;
import com.yubico.webauthn.exception.RegistrationFailedException;
import java.util.Optional;
public class WebAuthnRegistrationService {
private final RelyingParty relyingParty;
private final CredentialRepository credentialRepository;
public WebAuthnRegistrationService(String rpId, String rpName,
CredentialRepository credentialRepository) {
this.credentialRepository = credentialRepository;
this.relyingParty = RelyingParty.builder()
.identity(rpId)
.credentialRepository(credentialRepository)
.origins(Collections.singleton("https://yourdomain.com"))
.build();
}
public PublicKeyCredentialCreationOptions startRegistration(
String username, String displayName) {
UserIdentity userIdentity = UserIdentity.builder()
.name(username)
.displayName(displayName)
.id(generateUserId(username))
.build();
return relyingParty.startRegistration(
StartRegistrationOptions.builder()
.user(userIdentity)
.build()
);
}
public RegistrationResult finishRegistration(
PublicKeyCredentialCreationOptions request,
PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> response)
throws RegistrationFailedException {
return relyingParty.finishRegistration(
FinishRegistrationOptions.builder()
.request(request)
.response(response)
.build()
);
}
private ByteArray generateUserId(String username) {
// Generate a unique user ID
return new ByteArray(username.getBytes());
}
}
WebAuthn Authentication
public class WebAuthnAuthenticationService {
private final RelyingParty relyingParty;
private final CredentialRepository credentialRepository;
public WebAuthnAuthenticationService(String rpId, CredentialRepository credentialRepository) {
this.credentialRepository = credentialRepository;
this.relyingParty = RelyingParty.builder()
.identity(rpId)
.credentialRepository(credentialRepository)
.origins(Collections.singleton("https://yourdomain.com"))
.build();
}
public AssertionRequest startAuthentication(String username) {
return relyingParty.startAssertion(
StartAssertionOptions.builder()
.username(Optional.of(username))
.build()
);
}
public AssertionResult finishAuthentication(
AssertionRequest request,
PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> response)
throws AuthenticationFailedException {
return relyingParty.finishAssertion(
FinishAssertionOptions.builder()
.request(request)
.response(response)
.build()
);
}
}
Credential Repository Implementation
import com.yubico.webauthn.CredentialRepository;
import com.yubico.webauthn.data.ByteArray;
import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
import com.yubico.webauthn.data.PublicKeyCredentialUserEntity;
import java.util.*;
public class InMemoryCredentialRepository implements CredentialRepository {
private final Map<String, Set<PublicKeyCredentialDescriptor>> userCredentials = new HashMap<>();
private final Map<ByteArray, PublicKeyCredentialUserEntity> userHandles = new HashMap<>();
@Override
public Set<PublicKeyCredentialDescriptor> getCredentialIdsForUsername(String username) {
return userCredentials.getOrDefault(username, Collections.emptySet());
}
@Override
public Optional<ByteArray> getUserHandleForUsername(String username) {
return userHandles.entrySet().stream()
.filter(entry -> entry.getValue().getName().equals(username))
.map(Map.Entry::getKey)
.findFirst();
}
@Override
public Optional<String> getUsernameForUserHandle(ByteArray userHandle) {
return Optional.ofNullable(userHandles.get(userHandle))
.map(PublicKeyCredentialUserEntity::getName);
}
@Override
public Optional<PublicKeyCredentialUserEntity> getUserEntityForUsername(String username) {
return getUserHandleForUsername(username)
.map(userHandles::get);
}
public void addCredential(String username, PublicKeyCredentialDescriptor credential) {
userCredentials.computeIfAbsent(username, k -> new HashSet<>()).add(credential);
}
public void addUserHandle(PublicKeyCredentialUserEntity userEntity) {
userHandles.put(userEntity.getId(), userEntity);
}
}
Spring Security Integration
YubiKey Authentication Provider
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
@Component
public class YubiKeyAuthenticationProvider implements AuthenticationProvider {
private final UserDetailsService userDetailsService;
private final YubiKeyOTPValidator yubiKeyValidator;
public YubiKeyAuthenticationProvider(UserDetailsService userDetailsService,
YubiKeyOTPValidator yubiKeyValidator) {
this.userDetailsService = userDetailsService;
this.yubiKeyValidator = yubiKeyValidator;
}
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
String username = authentication.getName();
String credentials = (String) authentication.getCredentials();
// Extract password and OTP (assuming format: "password:yubikeyOTP")
String[] credParts = credentials.split(":");
if (credParts.length != 2) {
throw new BadCredentialsException("Invalid credentials format");
}
String password = credParts[0];
String yubiKeyOTP = credParts[1];
// Validate user credentials
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (!passwordMatches(password, userDetails.getPassword())) {
throw new BadCredentialsException("Invalid password");
}
// Validate YubiKey OTP
if (!yubiKeyValidator.validateOTP(yubiKeyOTP)) {
throw new BadCredentialsException("Invalid YubiKey OTP");
}
return new UsernamePasswordAuthenticationToken(
userDetails, credentials, userDetails.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
private boolean passwordMatches(String rawPassword, String encodedPassword) {
// Implement password matching logic
return true; // Simplified for example
}
}
Spring Boot Configuration
Application Properties
# YubiKey Configuration yubico.client.id=your-client-id yubico.secret.key=your-secret-key yubico.api.urls=https://api.yubico.com/wsapi/2.0/verify # WebAuthn Configuration webauthn.rp.id=your-domain.com webauthn.rp.name=Your Application Name
Spring Configuration Class
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
import java.util.List;
@Configuration
public class YubiKeyConfig {
@Value("${yubico.client.id}")
private String clientId;
@Value("${yubico.secret.key}")
private String secretKey;
@Value("${yubico.api.urls}")
private String[] apiUrls;
@Bean
public YubiKeyOTPValidator yubiKeyOTPValidator() {
AdvancedYubiKeyValidator validator = new AdvancedYubiKeyValidator(
clientId, secretKey, getAllowedYubiKeyIds());
if (apiUrls != null && apiUrls.length > 0) {
validator.setSyncApiUrls(Arrays.asList(apiUrls));
}
return validator;
}
private List<String> getAllowedYubiKeyIds() {
// Load from database or configuration
return Arrays.asList(
"cccccc123456",
"cccccc789012"
);
}
}
Complete Example: YubiKey Protected Login
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/auth")
public class YubiKeyAuthController {
private final YubiKeyOTPValidator yubiKeyValidator;
private final WebAuthnRegistrationService webAuthnService;
public YubiKeyAuthController(YubiKeyOTPValidator yubiKeyValidator,
WebAuthnRegistrationService webAuthnService) {
this.yubiKeyValidator = yubiKeyValidator;
this.webAuthnService = webAuthnService;
}
@PostMapping("/login")
public ResponseEntity<AuthResponse> login(@RequestBody LoginRequest request) {
AdvancedYubiKeyValidator.ValidationResult result =
yubiKeyValidator.validateYubiKey(request.getYubiKeyOTP());
if (result.isValid()) {
// Generate JWT token or session
String token = generateAuthToken(request.getUsername());
return ResponseEntity.ok(new AuthResponse(true, "Login successful", token));
} else {
return ResponseEntity.status(401)
.body(new AuthResponse(false, result.getMessage(), null));
}
}
@PostMapping("/webauthn/start-registration")
public PublicKeyCredentialCreationOptions startWebAuthnRegistration(
@RequestParam String username) {
return webAuthnService.startRegistration(username, username);
}
@PostMapping("/webauthn/finish-registration")
public ResponseEntity<String> finishWebAuthnRegistration(
@RequestBody RegistrationFinishRequest request) {
try {
webAuthnService.finishRegistration(request.getCreationOptions(),
request.getCredential());
return ResponseEntity.ok("Registration successful");
} catch (Exception e) {
return ResponseEntity.badRequest().body("Registration failed: " + e.getMessage());
}
}
// DTO classes
public static class LoginRequest {
private String username;
private String yubiKeyOTP;
// Getters and setters
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getYubiKeyOTP() { return yubiKeyOTP; }
public void setYubiKeyOTP(String yubiKeyOTP) { this.yubiKeyOTP = yubiKeyOTP; }
}
public static class AuthResponse {
private boolean success;
private String message;
private String token;
// Constructor, getters and setters
public AuthResponse(boolean success, String message, String token) {
this.success = success;
this.message = message;
this.token = token;
}
public boolean isSuccess() { return success; }
public String getMessage() { return message; }
public String getToken() { return token; }
}
public static class RegistrationFinishRequest {
private PublicKeyCredentialCreationOptions creationOptions;
private PublicKeyCredential<AuthenticatorAttestationResponse,
ClientRegistrationExtensionOutputs> credential;
// Getters and setters
}
private String generateAuthToken(String username) {
// Implement JWT token generation
return "generated-jwt-token";
}
}
Best Practices and Security Considerations
1. Secure Storage
- Store YubiKey mappings securely
- Use encryption for sensitive data
- Implement proper key rotation
2. Error Handling
- Don't expose detailed error messages to clients
- Log security events appropriately
- Implement rate limiting
3. Fallback Mechanisms
- Provide backup authentication methods
- Implement account recovery processes
- Consider multiple YubiKey registration per user
Conclusion
YubiKey integration in Java provides robust two-factor authentication that significantly enhances application security. The Yubico Java libraries offer comprehensive support for both traditional OTP validation and modern FIDO2/WebAuthn standards. By following the patterns and examples provided, you can implement secure, user-friendly authentication that leverages hardware security keys for protection against phishing and credential theft.
The integration works well with Spring Security and can be adapted to various application architectures, from traditional web applications to microservices and API-based systems.