YubiKey Integration in Java: Complete Guide

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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper