Hardware token authentication provides robust security through physical devices that generate one-time passwords (OTP) or use public key cryptography. This implementation covers various hardware token standards including FIDO2/WebAuthn, YubiKey, and TOTP-based tokens.
Supported Hardware Token Standards
- FIDO2/WebAuthn - Modern standard for passwordless authentication
- U2F - Universal 2nd Factor authentication
- TOTP/HOTP - Time-based and HMAC-based one-time passwords
- YubiKey OTP - Yubico's proprietary OTP format
- PIV/Smart Cards - Personal Identity Verification standards
Dependencies and Setup
Maven Configuration:
<dependencies> <!-- FIDO2/WebAuthn --> <dependency> <groupId>com.webauthn4j</groupId> <artifactId>webauthn4j-spring-security</artifactId> <version>0.21.0.RELEASE</version> </dependency> <dependency> <groupId>com.webauthn4j</groupId> <artifactId>webauthn4j-core</artifactId> <version>0.21.0.RELEASE</version> </dependency> <!-- YubiKit for YubiKey --> <dependency> <groupId>com.yubico</groupId> <artifactId>yubikit</artifactId> <version>2.3.0</version> </dependency> <!-- TOTP/HOTP --> <dependency> <groupId>com.warrenstrange</groupId> <artifactId>googleauth</artifactId> <version>1.5.0</version> </dependency> <!-- Bouncy Castle for cryptography --> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk18on</artifactId> <version>1.76</version> </dependency> <!-- Spring Security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- USB/HID Communication --> <dependency> <groupId>org.hid4java</groupId> <artifactId>hid4java</artifactId> <version>0.7.0</version> </dependency> </dependencies>
Core Configuration
HardwareTokenProperties.java:
@Configuration
@ConfigurationProperties(prefix = "hardware-token")
@Data
public class HardwareTokenProperties {
private FidoConfig fido = new FidoConfig();
private TotpConfig totp = new TotpConfig();
private YubikeyConfig yubikey = new YubikeyConfig();
private GeneralConfig general = new GeneralConfig();
@Data
public static class FidoConfig {
private String rpId = "localhost";
private String rpName = "My Application";
private String origin = "https://localhost:8443";
private List<String> allowedOrigins = Arrays.asList("https://localhost:8443");
private int timeout = 60000;
private boolean requireResidentKey = false;
private boolean requireUserVerification = true;
private List<String> supportedAlgorithms = Arrays.asList("ES256", "RS256");
}
@Data
public static class TotpConfig {
private int timeStep = 30;
private int codeLength = 6;
private int windowSize = 3;
private int secretLength = 20;
}
@Data
public static class YubikeyConfig {
private String clientId;
private String secretKey;
private List<String> apiUrls = 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"
);
private int timeout = 5000;
}
@Data
public static class GeneralConfig {
private int maxAttempts = 3;
private int lockoutMinutes = 30;
private boolean requireBackupCodes = true;
private int backupCodeCount = 10;
}
}
Token Metadata and Management
HardwareToken.java:
@Data
@AllArgsConstructor
public class HardwareToken {
private String id;
private String userId;
private TokenType type;
private String name;
private String publicKey; // For FIDO2 tokens
private String secret; // For TOTP tokens
private String credentialId; // For FIDO2
private int counter; // For U2F/HOTP
private Instant registeredAt;
private Instant lastUsedAt;
private boolean enabled;
private Map<String, String> metadata;
public enum TokenType {
FIDO2, U2F, TOTP, YUBIKEY_OTP, YUBIKEY_FIDO, SMART_CARD
}
public boolean isFidoToken() {
return type == TokenType.FIDO2 || type == TokenType.U2F || type == TokenType.YUBIKEY_FIDO;
}
public boolean isOtpToken() {
return type == TokenType.TOTP || type == TokenType.YUBIKEY_OTP;
}
}
TokenRegistration.java:
@Data
@Builder
public class TokenRegistration {
private String requestId;
private String userId;
private HardwareToken.TokenType tokenType;
private Instant expiresAt;
private Map<String, Object> challengeData;
private TokenRegistrationStatus status;
public enum TokenRegistrationStatus {
PENDING_CHALLENGE, COMPLETED, EXPIRED, FAILED
}
public boolean isValid() {
return status == TokenRegistrationStatus.PENDING_CHALLENGE &&
expiresAt.isAfter(Instant.now());
}
}
FIDO2/WebAuthn Implementation
Fido2Service.java:
@Service
@Slf4j
public class Fido2Service {
private final HardwareTokenProperties properties;
private final TokenRepository tokenRepository;
private final WebAuthnManager webAuthnManager;
private final ObjectMapper objectMapper;
public Fido2Service(HardwareTokenProperties properties,
TokenRepository tokenRepository) {
this.properties = properties;
this.tokenRepository = tokenRepository;
this.webAuthnManager = createWebAuthnManager();
this.objectMapper = new ObjectMapper();
}
private WebAuthnManager createWebAuthnManager() {
return WebAuthnManager.createNonStrictWebAuthnManager(
new DefaultCOSEKeyConfiguration(),
new DefaultMetadataBLOBProvider(),
new DefaultAttestationTrustworthinessValidator()
);
}
public PublicKeyCredentialCreationOptions generateRegistrationOptions(String userId,
String username) {
try {
// Generate challenge
byte[] challenge = generateRandomBytes(32);
// User information
UserIdentity userIdentity = new UserIdentity(
userId,
username,
username,
generateRandomBytes(64) // user handle
);
// Relying Party information
RelyingPartyIdentity rpIdentity = new RelyingPartyIdentity(
properties.getFido().getRpId(),
properties.getFido().getRpName()
);
// Public key parameters
List<PublicKeyCredentialParameters> pubKeyCredParams = Arrays.asList(
new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, COSEAlgorithmIdentifier.ES256),
new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, COSEAlgorithmIdentifier.RS256)
);
// Create registration options
return new PublicKeyCredentialCreationOptions(
rpIdentity,
userIdentity,
challenge,
pubKeyCredParams,
properties.getFido().getTimeout(),
null, // exclude credentials
null, // authenticator selection
null, // attestation preference
null // extensions
);
} catch (Exception e) {
throw new TokenRegistrationException("Failed to generate registration options", e);
}
}
public HardwareToken verifyRegistrationResponse(String userId,
String clientDataJson,
String attestationObject) {
try {
// Parse client data
CollectedClientData clientData = objectMapper.readValue(
clientDataJson, CollectedClientData.class);
// Parse attestation object
AttestationObject attestationObj = webAuthnManager.parse(attestationObject);
// Verify registration
RegistrationData registrationData = webAuthnManager.verify(
new RegistrationParameters(
clientData,
attestationObj,
Collections.emptyList(), // exclude credentials
properties.getFido().isRequireUserVerification()
)
);
// Extract credential information
AuthenticatorData authData = attestationObj.getAuthenticatorData();
String credentialId = Base64.getUrlEncoder().encodeToString(
authData.getAttestedCredentialData().getCredentialId()
);
// Create hardware token record
HardwareToken token = new HardwareToken(
UUID.randomUUID().toString(),
userId,
HardwareToken.TokenType.FIDO2,
"FIDO2 Security Key",
objectMapper.writeValueAsString(registrationData),
null,
credentialId,
0,
Instant.now(),
null,
true,
Map.of(
"aaguid", authData.getAttestedCredentialData().getAaguid().toString(),
"signCount", String.valueOf(authData.getSignCount())
)
);
return tokenRepository.save(token);
} catch (Exception e) {
throw new TokenVerificationException("FIDO2 registration verification failed", e);
}
}
public AssertionRequest generateAuthenticationRequest(String userId) {
try {
// Get user's registered tokens
List<HardwareToken> userTokens = tokenRepository.findByUserIdAndTypeAndEnabled(
userId, HardwareToken.TokenType.FIDO2, true);
List<PublicKeyCredentialDescriptor> allowCredentials = userTokens.stream()
.map(token -> new PublicKeyCredentialDescriptor(
PublicKeyCredentialType.PUBLIC_KEY,
Base64.getUrlDecoder().decode(token.getCredentialId()),
Collections.emptySet()
))
.collect(Collectors.toList());
// Generate challenge
byte[] challenge = generateRandomBytes(32);
return new AssertionRequest(
challenge,
properties.getFido().getTimeout(),
properties.getFido().getRpId(),
allowCredentials,
null, // user verification
null // extensions
);
} catch (Exception e) {
throw new TokenAuthenticationException("Failed to generate authentication request", e);
}
}
public boolean verifyAuthenticationResponse(String userId,
String credentialId,
String clientDataJson,
String authenticatorData,
String signature) {
try {
// Get the token
HardwareToken token = tokenRepository.findByCredentialId(credentialId)
.orElseThrow(() -> new TokenNotFoundException("Token not found"));
if (!token.getUserId().equals(userId)) {
throw new TokenAuthenticationException("Token does not belong to user");
}
// Parse client data
CollectedClientData clientData = objectMapper.readValue(
clientDataJson, CollectedClientData.class);
// Parse authenticator data
AuthenticatorData authData = AuthenticatorData.parse(
Base64.getUrlDecoder().decode(authenticatorData));
// Verify authentication
AuthenticationData authnData = new AuthenticationData(
clientData,
authData,
Base64.getUrlDecoder().decode(signature)
);
AuthenticationParameters params = new AuthenticationParameters(
new WebAuthnAuthenticationContext(
properties.getFido().getRpId(),
clientData,
null, // token binding
properties.getFido().isRequireUserVerification()
),
Collections.singletonList(parseRegistrationData(token)),
false // user verification required
);
AuthenticationResult result = webAuthnManager.verify(authnData, params);
if (result.isSuccess()) {
// Update token counter
token.setCounter(authData.getSignCount());
token.setLastUsedAt(Instant.now());
tokenRepository.save(token);
return true;
}
return false;
} catch (Exception e) {
throw new TokenVerificationException("FIDO2 authentication verification failed", e);
}
}
private RegistrationData parseRegistrationData(HardwareToken token) {
try {
return objectMapper.readValue(token.getPublicKey(), RegistrationData.class);
} catch (Exception e) {
throw new TokenDataException("Failed to parse token registration data", e);
}
}
private byte[] generateRandomBytes(int length) {
byte[] bytes = new byte[length];
new SecureRandom().nextBytes(bytes);
return bytes;
}
}
TOTP/HOTP Implementation
TotpService.java:
@Service
@Slf4j
public class TotpService {
private final HardwareTokenProperties properties;
private final TokenRepository tokenRepository;
public TotpService(HardwareTokenProperties properties,
TokenRepository tokenRepository) {
this.properties = properties;
this.tokenRepository = tokenRepository;
}
public TotpRegistration generateTotpSecret(String userId, String tokenName) {
try {
// Generate random secret
byte[] secretBytes = generateRandomBytes(properties.getTotp().getSecretLength());
String secret = Base32.encode(secretBytes);
// Create provisioning URI for authenticator apps
String provisioningUri = generateProvisioningUri(userId, secret, tokenName);
return TotpRegistration.builder()
.secret(secret)
.provisioningUri(provisioningUri)
.qrCodeData(generateQrCode(provisioningUri))
.build();
} catch (Exception e) {
throw new TokenGenerationException("Failed to generate TOTP secret", e);
}
}
public HardwareToken registerTotpToken(String userId, String tokenName, String secret) {
try {
// Verify the secret is valid
if (secret == null || secret.length() < 16) {
throw new InvalidTokenException("Invalid TOTP secret");
}
HardwareToken token = new HardwareToken(
UUID.randomUUID().toString(),
userId,
HardwareToken.TokenType.TOTP,
tokenName,
null,
encryptSecret(secret),
null,
0,
Instant.now(),
null,
true,
Map.of("algorithm", "SHA1", "digits", "6", "period", "30")
);
return tokenRepository.save(token);
} catch (Exception e) {
throw new TokenRegistrationException("Failed to register TOTP token", e);
}
}
public boolean verifyTotpCode(String userId, String code) {
try {
List<HardwareToken> tokens = tokenRepository.findByUserIdAndTypeAndEnabled(
userId, HardwareToken.TokenType.TOTP, true);
for (HardwareToken token : tokens) {
if (verifyTokenCode(token, code)) {
token.setLastUsedAt(Instant.now());
tokenRepository.save(token);
return true;
}
}
return false;
} catch (Exception e) {
throw new TokenVerificationException("TOTP verification failed", e);
}
}
private boolean verifyTokenCode(HardwareToken token, String code) {
try {
String secret = decryptSecret(token.getSecret());
// Calculate current and nearby time steps
long currentTimeStep = Instant.now().getEpochSecond() / properties.getTotp().getTimeStep();
for (int i = -properties.getTotp().getWindowSize();
i <= properties.getTotp().getWindowSize(); i++) {
long timeStep = currentTimeStep + i;
String calculatedCode = calculateCode(secret, timeStep);
if (calculatedCode.equals(code)) {
// Prevent code reuse
if (timeStep > token.getCounter()) {
token.setCounter((int) timeStep);
return true;
}
}
}
return false;
} catch (Exception e) {
log.error("TOTP code verification failed for token: {}", token.getId(), e);
return false;
}
}
private String calculateCode(String secret, long timeStep) {
try {
byte[] key = Base32.decode(secret);
byte[] data = new byte[8];
for (int i = 8; i-- > 0; timeStep >>>= 8) {
data[i] = (byte) timeStep;
}
// HMAC-SHA1
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(new SecretKeySpec(key, "HmacSHA1"));
byte[] hash = mac.doFinal(data);
// Dynamic truncation
int offset = hash[hash.length - 1] & 0xF;
long binary = ((hash[offset] & 0x7F) << 24) |
((hash[offset + 1] & 0xFF) << 16) |
((hash[offset + 2] & 0xFF) << 8) |
(hash[offset + 3] & 0xFF);
long otp = binary % (long) Math.pow(10, properties.getTotp().getCodeLength());
return String.format("%0" + properties.getTotp().getCodeLength() + "d", otp);
} catch (Exception e) {
throw new TokenCalculationException("Failed to calculate TOTP code", e);
}
}
private String generateProvisioningUri(String userId, String secret, String tokenName) {
return String.format("otpauth://totp/%s:%s?secret=%s&issuer=%s&algorithm=SHA1&digits=6&period=30",
properties.getFido().getRpName(), userId, secret, properties.getFido().getRpName());
}
private String generateQrCode(String provisioningUri) {
try {
QRCodeWriter qrCodeWriter = new QRCodeWriter();
BitMatrix bitMatrix = qrCodeWriter.encode(provisioningUri, BarcodeFormat.QR_CODE, 200, 200);
ByteArrayOutputStream pngOutputStream = new ByteArrayOutputStream();
MatrixToImageWriter.writeToStream(bitMatrix, "PNG", pngOutputStream);
return Base64.getEncoder().encodeToString(pngOutputStream.toByteArray());
} catch (Exception e) {
log.warn("Failed to generate QR code", e);
return null;
}
}
private String encryptSecret(String secret) {
// Implement secret encryption (e.g., using AES)
return secret; // Simplified for example
}
private String decryptSecret(String encryptedSecret) {
// Implement secret decryption
return encryptedSecret; // Simplified for example
}
private byte[] generateRandomBytes(int length) {
byte[] bytes = new byte[length];
new SecureRandom().nextBytes(bytes);
return bytes;
}
@Data
@Builder
public static class TotpRegistration {
private String secret;
private String provisioningUri;
private String qrCodeData;
}
}
YubiKey Implementation
YubikeyService.java:
@Service
@Slf4j
public class YubikeyService {
private final HardwareTokenProperties properties;
private final TokenRepository tokenRepository;
private final YubicoClient yubicoClient;
public YubikeyService(HardwareTokenProperties properties,
TokenRepository tokenRepository) {
this.properties = properties;
this.tokenRepository = tokenRepository;
this.yubicoClient = createYubicoClient();
}
private YubicoClient createYubicoClient() {
return YubicoClient.getClient(
properties.getYubikey().getClientId(),
properties.getYubikey().getSecretKey()
);
}
public boolean verifyYubikeyOtp(String userId, String otp) {
try {
// Validate OTP format
if (!isValidYubikeyOtp(otp)) {
return false;
}
// Extract public ID from OTP
String publicId = otp.substring(0, otp.length() - 32);
// Check if token is registered to user
Optional<HardwareToken> token = tokenRepository.findByUserIdAndMetadataValue(
userId, "publicId", publicId);
if (token.isEmpty()) {
// Verify with YubiCloud and register if valid
return verifyAndRegisterToken(userId, otp, publicId);
}
// Verify OTP with YubiCloud
YubicoResponse response = yubicoClient.verify(otp);
if (response.isOk()) {
HardwareToken hardwareToken = token.get();
hardwareToken.setLastUsedAt(Instant.now());
hardwareToken.setCounter(response.getCounter());
tokenRepository.save(hardwareToken);
return true;
}
return false;
} catch (Exception e) {
throw new TokenVerificationException("YubiKey OTP verification failed", e);
}
}
public HardwareToken registerYubikey(String userId, String tokenName, String publicId) {
try {
// Verify the public ID is valid
if (!isValidPublicId(publicId)) {
throw new InvalidTokenException("Invalid YubiKey public ID");
}
HardwareToken token = new HardwareToken(
UUID.randomUUID().toString(),
userId,
HardwareToken.TokenType.YUBIKEY_OTP,
tokenName,
null,
null,
null,
0,
Instant.now(),
null,
true,
Map.of("publicId", publicId, "version", "5")
);
return tokenRepository.save(token);
} catch (Exception e) {
throw new TokenRegistrationException("Failed to register YubiKey", e);
}
}
private boolean verifyAndRegisterToken(String userId, String otp, String publicId) {
try {
// Verify with YubiCloud
YubicoResponse response = yubicoClient.verify(otp);
if (response.isOk()) {
// Auto-register the token
registerYubikey(userId, "YubiKey " + publicId, publicId);
return true;
}
return false;
} catch (Exception e) {
log.error("Failed to verify and register YubiKey", e);
return false;
}
}
private boolean isValidYubikeyOtp(String otp) {
// YubiKey OTP is 32-48 characters, modhex encoded
return otp != null && otp.length() >= 32 && otp.length() <= 48 &&
otp.matches("[cbdefghijklnrtuv]+");
}
private boolean isValidPublicId(String publicId) {
// Public ID is 12 characters, modhex encoded
return publicId != null && publicId.length() == 12 &&
publicId.matches("[cbdefghijklnrtuv]+");
}
public YubicoDeviceInfo getDeviceInfo(String publicId) {
try {
// Use YubiKit to get device information
// This would typically involve USB communication
return YubicoDeviceInfo.builder()
.publicId(publicId)
.version("5.4.3")
.serialNumber(extractSerialNumber(publicId))
.build();
} catch (Exception e) {
log.warn("Failed to get YubiKey device info", e);
return null;
}
}
private String extractSerialNumber(String publicId) {
// Extract serial number from public ID (device-specific)
try {
byte[] decoded = ModHex.decode(publicId);
return Integer.toString(ByteBuffer.wrap(decoded).getInt());
} catch (Exception e) {
return "unknown";
}
}
@Data
@Builder
public static class YubicoDeviceInfo {
private String publicId;
private String version;
private String serialNumber;
private String formFactor;
}
}
Token Management Service
HardwareTokenService.java:
@Service
@Slf4j
public class HardwareTokenService {
private final TokenRepository tokenRepository;
private final Fido2Service fido2Service;
private final TotpService totpService;
private final YubikeyService yubikeyService;
private final HardwareTokenProperties properties;
public HardwareTokenService(TokenRepository tokenRepository,
Fido2Service fido2Service,
TotpService totpService,
YubikeyService yubikeyService,
HardwareTokenProperties properties) {
this.tokenRepository = tokenRepository;
this.fido2Service = fido2Service;
this.totpService = totpService;
this.yubikeyService = yubikeyService;
this.properties = properties;
}
public List<HardwareToken> getUserTokens(String userId) {
return tokenRepository.findByUserIdAndEnabled(userId, true);
}
public TokenRegistration startTokenRegistration(String userId,
HardwareToken.TokenType tokenType,
String tokenName) {
TokenRegistration registration = TokenRegistration.builder()
.requestId(UUID.randomUUID().toString())
.userId(userId)
.tokenType(tokenType)
.expiresAt(Instant.now().plusSeconds(300)) // 5 minutes
.status(TokenRegistrationStatus.PENDING_CHALLENGE)
.build();
// Generate challenge based on token type
Map<String, Object> challengeData = new HashMap<>();
switch (tokenType) {
case FIDO2:
PublicKeyCredentialCreationOptions options =
fido2Service.generateRegistrationOptions(userId, tokenName);
challengeData.put("fido2Options", options);
break;
case TOTP:
TotpService.TotpRegistration totpReg =
totpService.generateTotpSecret(userId, tokenName);
challengeData.put("totpRegistration", totpReg);
break;
case YUBIKEY_OTP:
challengeData.put("instructions",
"Insert your YubiKey and touch the gold button");
break;
default:
throw new UnsupportedTokenTypeException("Unsupported token type: " + tokenType);
}
registration.setChallengeData(challengeData);
return registration;
}
public HardwareToken completeTokenRegistration(String requestId,
Map<String, Object> verificationData) {
// This would typically retrieve from a temporary store
// For simplicity, we'll assume the registration is validated immediately
// Implementation would vary based on token type
throw new UnsupportedOperationException("Implementation specific");
}
public boolean verifyToken(String userId,
HardwareToken.TokenType tokenType,
Map<String, Object> verificationData) {
try {
switch (tokenType) {
case FIDO2:
return verifyFido2Token(userId, verificationData);
case TOTP:
String code = (String) verificationData.get("code");
return totpService.verifyTotpCode(userId, code);
case YUBIKEY_OTP:
String otp = (String) verificationData.get("otp");
return yubikeyService.verifyYubikeyOtp(userId, otp);
default:
throw new UnsupportedTokenTypeException("Unsupported token type: " + tokenType);
}
} catch (Exception e) {
log.error("Token verification failed for user: {}", userId, e);
return false;
}
}
private boolean verifyFido2Token(String userId, Map<String, Object> verificationData) {
String credentialId = (String) verificationData.get("credentialId");
String clientDataJson = (String) verificationData.get("clientDataJSON");
String authenticatorData = (String) verificationData.get("authenticatorData");
String signature = (String) verificationData.get("signature");
return fido2Service.verifyAuthenticationResponse(
userId, credentialId, clientDataJson, authenticatorData, signature);
}
public void disableToken(String userId, String tokenId) {
HardwareToken token = tokenRepository.findByIdAndUserId(tokenId, userId)
.orElseThrow(() -> new TokenNotFoundException("Token not found"));
token.setEnabled(false);
tokenRepository.save(token);
log.info("Disabled hardware token {} for user {}", tokenId, userId);
}
public boolean hasActiveTokens(String userId) {
return tokenRepository.countByUserIdAndEnabled(userId, true) > 0;
}
public Map<HardwareToken.TokenType, Integer> getTokenStats(String userId) {
List<HardwareToken> tokens = getUserTokens(userId);
return tokens.stream()
.collect(Collectors.groupingBy(
HardwareToken::getType,
Collectors.collectingAndThen(Collectors.counting(), Long::intValue)
));
}
}
Spring Security Integration
HardwareTokenAuthenticationFilter.java:
@Component
@Slf4j
public class HardwareTokenAuthenticationFilter extends OncePerRequestFilter {
private final HardwareTokenService tokenService;
private final UserService userService;
public HardwareTokenAuthenticationFilter(HardwareTokenService tokenService,
UserService userService) {
this.tokenService = tokenService;
this.userService = userService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
if (!requiresHardwareTokenAuth(request)) {
filterChain.doFilter(request, response);
return;
}
try {
String username = getUsernameFromRequest(request);
HardwareToken.TokenType tokenType = getTokenTypeFromRequest(request);
Map<String, Object> verificationData = getVerificationDataFromRequest(request);
if (username == null || tokenType == null || verificationData == null) {
sendError(response, HttpStatus.BAD_REQUEST, "Missing authentication data");
return;
}
// Verify hardware token
boolean verified = tokenService.verifyToken(username, tokenType, verificationData);
if (verified) {
// Create authentication token
User user = userService.loadUserByUsername(username);
HardwareTokenAuthenticationToken authentication =
new HardwareTokenAuthenticationToken(user, tokenType, verificationData);
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
} else {
sendError(response, HttpStatus.UNAUTHORIZED, "Hardware token verification failed");
}
} catch (Exception e) {
log.error("Hardware token authentication failed", e);
sendError(response, HttpStatus.INTERNAL_SERVER_ERROR, "Authentication failed");
}
}
private boolean requiresHardwareTokenAuth(HttpServletRequest request) {
String path = request.getRequestURI();
return path.startsWith("/api/auth/hardware/") &&
"POST".equalsIgnoreCase(request.getMethod());
}
private String getUsernameFromRequest(HttpServletRequest request) {
return request.getHeader("X-Username");
}
private HardwareToken.TokenType getTokenTypeFromRequest(HttpServletRequest request) {
String typeStr = request.getHeader("X-Token-Type");
try {
return HardwareToken.TokenType.valueOf(typeStr.toUpperCase());
} catch (Exception e) {
return null;
}
}
private Map<String, Object> getVerificationDataFromRequest(HttpServletRequest request) {
try {
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(request.getInputStream(), Map.class);
} catch (Exception e) {
return null;
}
}
private void sendError(HttpServletResponse response, HttpStatus status, String message)
throws IOException {
response.setStatus(status.value());
response.setContentType("application/json");
Map<String, Object> error = Map.of(
"error", status.getReasonPhrase(),
"message", message,
"timestamp", Instant.now()
);
ObjectMapper mapper = new ObjectMapper();
response.getWriter().write(mapper.writeValueAsString(error));
}
}
HardwareTokenAuthenticationToken.java:
public class HardwareTokenAuthenticationToken extends AbstractAuthenticationToken {
private final User principal;
private final HardwareToken.TokenType tokenType;
private final Map<String, Object> verificationData;
public HardwareTokenAuthenticationToken(User principal,
HardwareToken.TokenType tokenType,
Map<String, Object> verificationData) {
super(principal.getAuthorities());
this.principal = principal;
this.tokenType = tokenType;
this.verificationData = verificationData;
setAuthenticated(true);
}
@Override
public Object getCredentials() {
return verificationData;
}
@Override
public Object getPrincipal() {
return principal;
}
public HardwareToken.TokenType getTokenType() {
return tokenType;
}
@Override
public String getName() {
return principal.getUsername();
}
}
REST API Controllers
HardwareTokenController.java:
@RestController
@RequestMapping("/api/hardware-tokens")
@Slf4j
public class HardwareTokenController {
private final HardwareTokenService tokenService;
private final Fido2Service fido2Service;
private final TotpService totpService;
public HardwareTokenController(HardwareTokenService tokenService,
Fido2Service fido2Service,
TotpService totpService) {
this.tokenService = tokenService;
this.fido2Service = fido2Service;
this.totpService = totpService;
}
@GetMapping
public ResponseEntity<List<HardwareToken>> getUserTokens(Authentication authentication) {
String userId = authentication.getName();
List<HardwareToken> tokens = tokenService.getUserTokens(userId);
return ResponseEntity.ok(tokens);
}
@PostMapping("/register/start")
public ResponseEntity<TokenRegistration> startRegistration(
@RequestBody StartRegistrationRequest request,
Authentication authentication) {
String userId = authentication.getName();
TokenRegistration registration = tokenService.startTokenRegistration(
userId, request.getTokenType(), request.getTokenName());
return ResponseEntity.ok(registration);
}
@PostMapping("/verify/fido2")
public ResponseEntity<VerificationResponse> verifyFido2(
@RequestBody Fido2VerificationRequest request,
Authentication authentication) {
String userId = authentication.getName();
Map<String, Object> verificationData = Map.of(
"credentialId", request.getCredentialId(),
"clientDataJSON", request.getClientDataJSON(),
"authenticatorData", request.getAuthenticatorData(),
"signature", request.getSignature()
);
boolean verified = tokenService.verifyToken(
userId, HardwareToken.TokenType.FIDO2, verificationData);
return ResponseEntity.ok(new VerificationResponse(verified));
}
@PostMapping("/verify/totp")
public ResponseEntity<VerificationResponse> verifyTotp(
@RequestBody TotpVerificationRequest request,
Authentication authentication) {
String userId = authentication.getName();
Map<String, Object> verificationData = Map.of("code", request.getCode());
boolean verified = tokenService.verifyToken(
userId, HardwareToken.TokenType.TOTP, verificationData);
return ResponseEntity.ok(new VerificationResponse(verified));
}
@DeleteMapping("/{tokenId}")
public ResponseEntity<Void> disableToken(@PathVariable String tokenId,
Authentication authentication) {
String userId = authentication.getName();
tokenService.disableToken(userId, tokenId);
return ResponseEntity.noContent().build();
}
// DTO classes
@Data
public static class StartRegistrationRequest {
private HardwareToken.TokenType tokenType;
private String tokenName;
}
@Data
public static class Fido2VerificationRequest {
private String credentialId;
private String clientDataJSON;
private String authenticatorData;
private String signature;
}
@Data
public static class TotpVerificationRequest {
private String code;
}
@Data
@AllArgsConstructor
public static class VerificationResponse {
private boolean verified;
}
}
Application Configuration
application.yml:
hardware-token:
general:
max-attempts: 3
lockout-minutes: 30
require-backup-codes: true
backup-code-count: 10
fido:
rp-id: localhost
rp-name: "My Secure Application"
origin: "https://localhost:8443"
timeout: 60000
require-user-verification: true
totp:
time-step: 30
code-length: 6
window-size: 3
secret-length: 20
yubikey:
client-id: ${YUBIKEY_CLIENT_ID:}
secret-key: ${YUBIKEY_SECRET_KEY:}
api-urls:
- "https://api.yubico.com/wsapi/2.0/verify"
- "https://api2.yubico.com/wsapi/2.0/verify"
server:
ssl:
enabled: true
key-store: classpath:keystore.p12
key-store-password: changeit
key-store-type: PKCS12
Best Practices
- Secure Storage - Encrypt token secrets and private keys
- Rate Limiting - Implement attempt limits to prevent brute force
- Audit Logging - Log all token registration and usage events
- Backup Codes - Provide fallback authentication methods
- Token Revocation - Allow users to remotely disable lost tokens
- Multi-token Support - Allow users to register multiple tokens
- Usability - Provide clear instructions and error messages
Conclusion
Hardware token authentication in Java provides:
- Strong security through physical token possession
- Multi-factor authentication support
- Standards compliance with FIDO2, TOTP, and other protocols
- Flexible integration with various token types
- User-friendly experience with proper UX considerations
By implementing the comprehensive hardware token authentication system shown above, organizations can significantly enhance their security posture while providing users with convenient and robust authentication options.