EdDSA (Edwards-curve Digital Signature Algorithm) is a modern, high-performance digital signature scheme that's particularly well-suited for JWT signing. This guide covers comprehensive EdDSA JWT implementation in Java.
Understanding EdDSA for JWT
Why EdDSA for JWT?
- Performance: Faster than RSA and ECDSA
- Security: Strong security with smaller keys
- Compact: Smaller signature sizes
- Deterministic: No need for random number generation during signing
Dependencies and Setup
Maven Configuration
<!-- pom.xml --> <project> <modelVersion>4.0.0</modelVersion> <groupId>com.jwt.eddsa</groupId> <artifactId>jwt-eddsa-demo</artifactId> <version>1.0.0</version> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <jjwt.version>0.12.3</jjwt.version> <bouncycastle.version>1.75</bouncycastle.version> </properties> <dependencies> <!-- JJWT for JWT handling --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>${jjwt.version}</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>${jjwt.version}</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>${jjwt.version}</version> <scope>runtime</scope> </dependency> <!-- Bouncy Castle for EdDSA support --> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk18on</artifactId> <version>${bouncycastle.version}</version> </dependency> <!-- For JSON processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> </dependency> <!-- Testing --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.10.0</version> <scope>test</scope> </dependency> </dependencies> </project> Core EdDSA JWT Implementation
Example 1: Key Pair Generation and Management
package com.jwt.eddsa; import org.bouncycastle.jce.provider.BouncyCastleProvider; import java.security.*; import java.security.spec.*; import java.util.Base64; public class EdDSAKeyGenerator { static { Security.addProvider(new BouncyCastleProvider()); } // EdDSA curve names public static final String ED25519 = "Ed25519"; public static final String ED448 = "Ed448"; /** * Generate EdDSA key pair */ public static KeyPair generateKeyPair(String algorithm) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { KeyPairGenerator keyGen = KeyPairGenerator.getInstance(algorithm, "BC"); if (ED25519.equals(algorithm)) { keyGen.initialize(255); } else if (ED448.equals(algorithm)) { keyGen.initialize(448); } else { throw new IllegalArgumentException("Unsupported algorithm: " + algorithm); } return keyGen.generateKeyPair(); } /** * Encode public key to Base64 string */ public static String encodePublicKey(PublicKey publicKey) { return Base64.getUrlEncoder().withoutPadding().encodeToString(publicKey.getEncoded()); } /** * Encode private key to Base64 string */ public static String encodePrivateKey(PrivateKey privateKey) { return Base64.getUrlEncoder().withoutPadding().encodeToString(privateKey.getEncoded()); } /** * Decode public key from Base64 string */ public static PublicKey decodePublicKey(String base64Key, String algorithm) throws GeneralSecurityException { byte[] keyBytes = Base64.getUrlDecoder().decode(base64Key); KeyFactory keyFactory = KeyFactory.getInstance(algorithm, "BC"); X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes); return keyFactory.generatePublic(keySpec); } /** * Decode private key from Base64 string */ public static PrivateKey decodePrivateKey(String base64Key, String algorithm) throws GeneralSecurityException { byte[] keyBytes = Base64.getUrlDecoder().decode(base64Key); KeyFactory keyFactory = KeyFactory.getInstance(algorithm, "BC"); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); return keyFactory.generatePrivate(keySpec); } /** * Generate key pair and return as encoded strings */ public static KeyPairStrings generateKeyPairStrings(String algorithm) throws GeneralSecurityException { KeyPair keyPair = generateKeyPair(algorithm); KeyPairStrings keyStrings = new KeyPairStrings(); keyStrings.setPublicKey(encodePublicKey(keyPair.getPublic())); keyStrings.setPrivateKey(encodePrivateKey(keyPair.getPrivate())); keyStrings.setAlgorithm(algorithm); return keyStrings; } /** * Get key information */ public static KeyInfo getKeyInfo(KeyPair keyPair) { KeyInfo info = new KeyInfo(); info.setAlgorithm(keyPair.getPublic().getAlgorithm()); info.setFormat(keyPair.getPublic().getFormat()); info.setEncodedLength(keyPair.getPublic().getEncoded().length); if (keyPair.getPublic() instanceof EdECPublicKey) { EdECPublicKey edKey = (EdECPublicKey) keyPair.getPublic(); info.setParams(edKey.getParams().getName()); } return info; } } // Data classes for key management class KeyPairStrings { private String publicKey; private String privateKey; private String algorithm; // Getters and setters public String getPublicKey() { return publicKey; } public void setPublicKey(String publicKey) { this.publicKey = publicKey; } public String getPrivateKey() { return privateKey; } public void setPrivateKey(String privateKey) { this.privateKey = privateKey; } public String getAlgorithm() { return algorithm; } public void setAlgorithm(String algorithm) { this.algorithm = algorithm; } } class KeyInfo { private String algorithm; private String format; private int encodedLength; private String params; // Getters and setters public String getAlgorithm() { return algorithm; } public void setAlgorithm(String algorithm) { this.algorithm = algorithm; } public String getFormat() { return format; } public void setFormat(String format) { this.format = format; } public int getEncodedLength() { return encodedLength; } public void setEncodedLength(int encodedLength) { this.encodedLength = encodedLength; } public String getParams() { return params; } public void setParams(String params) { this.params = params; } @Override public String toString() { return String.format( "KeyInfo{algorithm='%s', format='%s', encodedLength=%d, params='%s'}", algorithm, format, encodedLength, params ); } } Example 2: JWT Service with EdDSA
package com.jwt.eddsa; import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.SignatureException; import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; import java.time.Instant; import java.util.Date; import java.util.HashMap; import java.util.Map; public class EdDSAJwtService { private final String issuer; private final long accessTokenExpiration; private final long refreshTokenExpiration; public EdDSAJwtService(String issuer, long accessTokenExpiration, long refreshTokenExpiration) { this.issuer = issuer; this.accessTokenExpiration = accessTokenExpiration; this.refreshTokenExpiration = refreshTokenExpiration; } /** * Create access token with EdDSA signing */ public String createAccessToken(KeyPair keyPair, String subject, Map<String, Object> claims) { return createToken(keyPair, subject, claims, accessTokenExpiration); } /** * Create refresh token with EdDSA signing */ public String createRefreshToken(KeyPair keyPair, String subject) { Map<String, Object> claims = new HashMap<>(); claims.put("token_type", "refresh"); return createToken(keyPair, subject, claims, refreshTokenExpiration); } /** * Create JWT token with EdDSA signing */ private String createToken(KeyPair keyPair, String subject, Map<String, Object> claims, long expirationMs) { Instant now = Instant.now(); Instant expiration = now.plusMillis(expirationMs); JwtBuilder builder = Jwts.builder() .setIssuer(issuer) .setSubject(subject) .setIssuedAt(Date.from(now)) .setExpiration(Date.from(expiration)) .signWith(keyPair.getPrivate(), getSignatureAlgorithm(keyPair)); // Add custom claims if (claims != null) { claims.forEach(builder::claim); } // Add key ID if available String keyId = getKeyId(keyPair.getPublic()); if (keyId != null) { builder.setHeaderParam("kid", keyId); } return builder.compact(); } /** * Parse and validate JWT token */ public JwtClaims parseToken(String token, PublicKey publicKey) throws JwtException { try { Jws<Claims> jws = Jwts.parserBuilder() .setSigningKey(publicKey) .build() .parseClaimsJws(token); Claims claims = jws.getBody(); JwtClaims result = new JwtClaims(); result.setSubject(claims.getSubject()); result.setIssuer(claims.getIssuer()); result.setIssuedAt(claims.getIssuedAt().toInstant()); result.setExpiration(claims.getExpiration().toInstant()); result.setClaims(new HashMap<>(claims)); // Remove standard claims from custom claims result.getClaims().remove("sub"); result.getClaims().remove("iss"); result.getClaims().remove("iat"); result.getClaims().remove("exp"); return result; } catch (ExpiredJwtException e) { throw new JwtException("Token expired", e); } catch (UnsupportedJwtException e) { throw new JwtException("Unsupported token", e); } catch (MalformedJwtException e) { throw new JwtException("Malformed token", e); } catch (SignatureException e) { throw new JwtException("Invalid signature", e); } catch (IllegalArgumentException e) { throw new JwtException("Invalid token", e); } } /** * Refresh access token using refresh token */ public String refreshAccessToken(String refreshToken, KeyPair keyPair, PublicKey verificationKey) throws JwtException { JwtClaims claims = parseToken(refreshToken, verificationKey); if (!"refresh".equals(claims.getClaims().get("token_type"))) { throw new JwtException("Invalid refresh token"); } // Create new access token with same subject Map<String, Object> newClaims = new HashMap<>(claims.getClaims()); newClaims.remove("token_type"); // Remove refresh token specific claim return createAccessToken(keyPair, claims.getSubject(), newClaims); } /** * Validate token without parsing claims (fast validation) */ public boolean validateToken(String token, PublicKey publicKey) { try { Jwts.parserBuilder() .setSigningKey(publicKey) .build() .parseClaimsJws(token); return true; } catch (JwtException e) { return false; } } /** * Get signature algorithm based on key type */ private SignatureAlgorithm getSignatureAlgorithm(KeyPair keyPair) { String algorithm = keyPair.getPrivate().getAlgorithm(); switch (algorithm) { case "Ed25519": return SignatureAlgorithm.Ed25519; case "Ed448": return SignatureAlgorithm.Ed448; default: throw new IllegalArgumentException("Unsupported algorithm: " + algorithm); } } /** * Generate key ID from public key (SHA-256 hash) */ private String getKeyId(PublicKey publicKey) { try { java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-256"); byte[] hash = md.digest(publicKey.getEncoded()); return bytesToHex(hash).substring(0, 16); // Use first 16 chars as key ID } catch (Exception e) { return null; } } private String bytesToHex(byte[] bytes) { StringBuilder result = new StringBuilder(); for (byte b : bytes) { result.append(String.format("%02x", b)); } return result.toString(); } } // JWT Claims data class class JwtClaims { private String subject; private String issuer; private Instant issuedAt; private Instant expiration; private Map<String, Object> claims; // Getters and setters public String getSubject() { return subject; } public void setSubject(String subject) { this.subject = subject; } public String getIssuer() { return issuer; } public void setIssuer(String issuer) { this.issuer = issuer; } public Instant getIssuedAt() { return issuedAt; } public void setIssuedAt(Instant issuedAt) { this.issuedAt = issuedAt; } public Instant getExpiration() { return expiration; } public void setExpiration(Instant expiration) { this.expiration = expiration; } public Map<String, Object> getClaims() { return claims; } public void setClaims(Map<String, Object> claims) { this.claims = claims; } public boolean isExpired() { return Instant.now().isAfter(expiration); } public long getSecondsUntilExpiration() { return Instant.now().until(expiration, java.time.temporal.ChronoUnit.SECONDS); } } // Custom JWT exception class JwtException extends Exception { public JwtException(String message) { super(message); } public JwtException(String message, Throwable cause) { super(message, cause); } } Example 3: Advanced JWT Claims and Validation
package com.jwt.eddsa; import java.time.Instant; import java.util.*; public class AdvancedJwtService { private final EdDSAJwtService jwtService; public AdvancedJwtService(EdDSAJwtService jwtService) { this.jwtService = jwtService; } /** * Create token with advanced claims */ public String createAdvancedToken(KeyPair keyPair, TokenRequest request) throws JwtException { Map<String, Object> claims = new HashMap<>(); // Standard claims claims.put("token_type", request.getTokenType()); claims.put("scope", request.getScopes()); claims.put("aud", request.getAudience()); // Custom claims claims.put("user_id", request.getUserId()); claims.put("roles", request.getRoles()); claims.put("permissions", request.getPermissions()); // Security claims claims.put("auth_time", Instant.now().getEpochSecond()); claims.put("jti", UUID.randomUUID().toString()); // Unique token ID // Device context if (request.getDeviceContext() != null) { claims.put("device_id", request.getDeviceContext().getDeviceId()); claims.put("device_trust_level", request.getDeviceContext().getTrustLevel()); } // Add not before claim if specified if (request.getNotBefore() != null) { claims.put("nbf", request.getNotBefore().getEpochSecond()); } return jwtService.createAccessToken(keyPair, request.getSubject(), claims); } /** * Validate token with comprehensive checks */ public TokenValidationResult validateTokenComprehensive(String token, PublicKey publicKey, ValidationContext context) { TokenValidationResult result = new TokenValidationResult(); try { JwtClaims claims = jwtService.parseToken(token, publicKey); result.setClaims(claims); // Check expiration if (claims.isExpired()) { result.addError("Token expired"); } // Check issuer if (context.getExpectedIssuer() != null && !context.getExpectedIssuer().equals(claims.getIssuer())) { result.addError("Invalid issuer"); } // Check audience if (context.getExpectedAudience() != null && claims.getClaims().containsKey("aud")) { Object aud = claims.getClaims().get("aud"); if (aud instanceof String) { if (!context.getExpectedAudience().equals(aud)) { result.addError("Invalid audience"); } } else if (aud instanceof List) { if (!((List<?>) aud).contains(context.getExpectedAudience())) { result.addError("Invalid audience"); } } } // Check not before if (claims.getClaims().containsKey("nbf")) { Instant nbf = Instant.ofEpochSecond(((Number) claims.getClaims().get("nbf")).longValue()); if (Instant.now().isBefore(nbf)) { result.addError("Token not yet valid"); } } // Check token type if (context.getExpectedTokenType() != null) { Object tokenType = claims.getClaims().get("token_type"); if (!context.getExpectedTokenType().equals(tokenType)) { result.addError("Invalid token type"); } } // Check scopes if (context.getRequiredScopes() != null && !context.getRequiredScopes().isEmpty()) { Object scopeObj = claims.getClaims().get("scope"); if (scopeObj instanceof String) { Set<String> tokenScopes = new HashSet<>(Arrays.asList(((String) scopeObj).split(" "))); if (!tokenScopes.containsAll(context.getRequiredScopes())) { result.addError("Insufficient scopes"); } } } // Check custom business rules validateBusinessRules(claims, context, result); } catch (JwtException e) { result.addError("Token validation failed: " + e.getMessage()); } result.setValid(result.getErrors().isEmpty()); return result; } /** * Extract specific claim with type safety */ public <T> T getClaim(String token, PublicKey publicKey, String claimName, Class<T> claimType) throws JwtException { JwtClaims claims = jwtService.parseToken(token, publicKey); Object value = claims.getClaims().get(claimName); if (value == null) { return null; } if (!claimType.isInstance(value)) { throw new JwtException("Claim '" + claimName + "' is not of type " + claimType.getSimpleName()); } return claimType.cast(value); } /** * Get token metadata without validating signature */ public TokenMetadata getTokenMetadata(String token) { try { // Split JWT into parts String[] parts = token.split("\\."); if (parts.length != 3) { throw new IllegalArgumentException("Invalid JWT format"); } // Decode header String headerJson = new String(Base64.getUrlDecoder().decode(parts[0])); Map<String, Object> header = parseJson(headerJson); // Decode payload without validation String payloadJson = new String(Base64.getUrlDecoder().decode(parts[1])); Map<String, Object> payload = parseJson(payloadJson); TokenMetadata metadata = new TokenMetadata(); metadata.setAlgorithm((String) header.get("alg")); metadata.setKeyId((String) header.get("kid")); metadata.setType((String) header.get("typ")); if (payload.containsKey("exp")) { long exp = ((Number) payload.get("exp")).longValue(); metadata.setExpiration(Instant.ofEpochSecond(exp)); } if (payload.containsKey("sub")) { metadata.setSubject((String) payload.get("sub")); } metadata.setSignatureLength(parts[2].length()); return metadata; } catch (Exception e) { throw new JwtException("Failed to parse token metadata", e); } } private void validateBusinessRules(JwtClaims claims, ValidationContext context, TokenValidationResult result) { // Implement custom business logic validation // Example: Check if user is active Boolean isActive = (Boolean) claims.getClaims().get("user_active"); if (isActive != null && !isActive) { result.addError("User account is inactive"); } // Example: Check device trust level Object trustLevel = claims.getClaims().get("device_trust_level"); if (trustLevel != null && "low".equals(trustLevel) && context.isHighSecurityRequired()) { result.addError("Device trust level too low for this operation"); } // Example: Check token usage count if (claims.getClaims().containsKey("usage_count")) { int usageCount = ((Number) claims.getClaims().get("usage_count")).intValue(); if (usageCount > context.getMaxUsageCount()) { result.addError("Token usage limit exceeded"); } } } @SuppressWarnings("unchecked") private Map<String, Object> parseJson(String json) { try { com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); return mapper.readValue(json, Map.class); } catch (Exception e) { throw new RuntimeException("Failed to parse JSON", e); } } } // Request and result classes class TokenRequest { private String subject; private String userId; private String tokenType; private List<String> scopes; private String audience; private List<String> roles; private List<String> permissions; private DeviceContext deviceContext; private Instant notBefore; // Getters and setters public String getSubject() { return subject; } public void setSubject(String subject) { this.subject = subject; } public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public String getTokenType() { return tokenType; } public void setTokenType(String tokenType) { this.tokenType = tokenType; } public List<String> getScopes() { return scopes; } public void setScopes(List<String> scopes) { this.scopes = scopes; } public String getAudience() { return audience; } public void setAudience(String audience) { this.audience = audience; } public List<String> getRoles() { return roles; } public void setRoles(List<String> roles) { this.roles = roles; } public List<String> getPermissions() { return permissions; } public void setPermissions(List<String> permissions) { this.permissions = permissions; } public DeviceContext getDeviceContext() { return deviceContext; } public void setDeviceContext(DeviceContext deviceContext) { this.deviceContext = deviceContext; } public Instant getNotBefore() { return notBefore; } public void setNotBefore(Instant notBefore) { this.notBefore = notBefore; } } class DeviceContext { private String deviceId; private String deviceType; private String trustLevel; // Getters and setters public String getDeviceId() { return deviceId; } public void setDeviceId(String deviceId) { this.deviceId = deviceId; } public String getDeviceType() { return deviceType; } public void setDeviceType(String deviceType) { this.deviceType = deviceType; } public String getTrustLevel() { return trustLevel; } public void setTrustLevel(String trustLevel) { this.trustLevel = trustLevel; } } class ValidationContext { private String expectedIssuer; private String expectedAudience; private String expectedTokenType; private Set<String> requiredScopes; private boolean highSecurityRequired; private int maxUsageCount = Integer.MAX_VALUE; // Getters and setters public String getExpectedIssuer() { return expectedIssuer; } public void setExpectedIssuer(String expectedIssuer) { this.expectedIssuer = expectedIssuer; } public String getExpectedAudience() { return expectedAudience; } public void setExpectedAudience(String expectedAudience) { this.expectedAudience = expectedAudience; } public String getExpectedTokenType() { return expectedTokenType; } public void setExpectedTokenType(String expectedTokenType) { this.expectedTokenType = expectedTokenType; } public Set<String> getRequiredScopes() { return requiredScopes; } public void setRequiredScopes(Set<String> requiredScopes) { this.requiredScopes = requiredScopes; } public boolean isHighSecurityRequired() { return highSecurityRequired; } public void setHighSecurityRequired(boolean highSecurityRequired) { this.highSecurityRequired = highSecurityRequired; } public int getMaxUsageCount() { return maxUsageCount; } public void setMaxUsageCount(int maxUsageCount) { this.maxUsageCount = maxUsageCount; } } class TokenValidationResult { private boolean valid; private JwtClaims claims; private List<String> errors = new ArrayList<>(); // Getters and setters public boolean isValid() { return valid; } public void setValid(boolean valid) { this.valid = valid; } public JwtClaims getClaims() { return claims; } public void setClaims(JwtClaims claims) { this.claims = claims; } public List<String> getErrors() { return errors; } public void setErrors(List<String> errors) { this.errors = errors; } public void addError(String error) { this.errors.add(error); } public String getErrorSummary() { return String.join("; ", errors); } } class TokenMetadata { private String algorithm; private String keyId; private String type; private String subject; private Instant expiration; private int signatureLength; // Getters and setters public String getAlgorithm() { return algorithm; } public void setAlgorithm(String algorithm) { this.algorithm = algorithm; } public String getKeyId() { return keyId; } public void setKeyId(String keyId) { this.keyId = keyId; } public String getType() { return type; } public void setType(String type) { this.type = type; } public String getSubject() { return subject; } public void setSubject(String subject) { this.subject = subject; } public Instant getExpiration() { return expiration; } public void setExpiration(Instant expiration) { this.expiration = expiration; } public int getSignatureLength() { return signatureLength; } public void setSignatureLength(int signatureLength) { this.signatureLength = signatureLength; } } Example 4: Key Rotation and Management
package com.jwt.eddsa; import java.security.*; import java.time.Instant; import java.util.*; import java.util.concurrent.ConcurrentHashMap; public class KeyManager { private final Map<String, KeyEntry> keys = new ConcurrentHashMap<>(); private final String currentKeyId; public KeyManager() throws GeneralSecurityException { // Generate initial key pair KeyPair keyPair = EdDSAKeyGenerator.generateKeyPair(EdDSAKeyGenerator.ED25519); this.currentKeyId = addKey(keyPair, "Initial key", true); } /** * Add a new key to the manager */ public String addKey(KeyPair keyPair, String description, boolean setAsCurrent) throws GeneralSecurityException { String keyId = generateKeyId(keyPair.getPublic()); KeyEntry entry = new KeyEntry(); entry.setKeyPair(keyPair); entry.setKeyId(keyId); entry.setDescription(description); entry.setCreatedAt(Instant.now()); entry.setActive(true); keys.put(keyId, entry); if (setAsCurrent) { this.currentKeyId = keyId; } return keyId; } /** * Get current key pair */ public KeyPair getCurrentKeyPair() { KeyEntry entry = keys.get(currentKeyId); return entry != null ? entry.getKeyPair() : null; } /** * Get public key by key ID */ public PublicKey getPublicKey(String keyId) { KeyEntry entry = keys.get(keyId); return entry != null ? entry.getKeyPair().getPublic() : null; } /** * Rotate to a new key */ public String rotateKey() throws GeneralSecurityException { // Generate new key pair KeyPair newKeyPair = EdDSAKeyGenerator.generateKeyPair(EdDSAKeyGenerator.ED25519); String newKeyId = addKey(newKeyPair, "Rotated key", true); // Mark old keys as inactive after a grace period scheduleKeyDeactivation(currentKeyId); return newKeyId; } /** * Validate token with key rotation support */ public JwtClaims validateTokenWithKeyRotation(String token) throws JwtException { // Extract key ID from token header String keyId = extractKeyIdFromToken(token); if (keyId == null) { throw new JwtException("No key ID in token"); } // Try to get the public key PublicKey publicKey = getPublicKey(keyId); if (publicKey == null) { throw new JwtException("Unknown key ID: " + keyId); } KeyEntry keyEntry = keys.get(keyId); if (!keyEntry.isActive()) { throw new JwtException("Key is no longer active: " + keyId); } // Parse token with the appropriate key EdDSAJwtService jwtService = new EdDSAJwtService("issuer", 3600000, 86400000); return jwtService.parseToken(token, publicKey); } /** * Get all active keys */ public List<KeyEntry> getActiveKeys() { List<KeyEntry> activeKeys = new ArrayList<>(); for (KeyEntry entry : keys.values()) { if (entry.isActive()) { activeKeys.add(entry); } } return activeKeys; } /** * Get key statistics */ public KeyStatistics getKeyStatistics() { KeyStatistics stats = new KeyStatistics(); stats.setTotalKeys(keys.size()); int activeCount = 0; Instant oldestCreation = Instant.now(); for (KeyEntry entry : keys.values()) { if (entry.isActive()) { activeCount++; } if (entry.getCreatedAt().isBefore(oldestCreation)) { oldestCreation = entry.getCreatedAt(); } } stats.setActiveKeys(activeCount); stats.setInactiveKeys(keys.size() - activeCount); stats.setOldestKeyAge(java.time.Duration.between(oldestCreation, Instant.now())); return stats; } private String generateKeyId(PublicKey publicKey) throws GeneralSecurityException { MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] hash = md.digest(publicKey.getEncoded()); return bytesToHex(hash).substring(0, 16); } private String extractKeyIdFromToken(String token) { try { String[] parts = token.split("\\."); if (parts.length != 3) { return null; } String headerJson = new String(Base64.getUrlDecoder().decode(parts[0])); Map<String, Object> header = parseJson(headerJson); return (String) header.get("kid"); } catch (Exception e) { return null; } } private void scheduleKeyDeactivation(String keyId) { // Schedule key deactivation after grace period (e.g., 24 hours) Timer timer = new Timer(true); timer.schedule(new TimerTask() { @Override public void run() { KeyEntry entry = keys.get(keyId); if (entry != null) { entry.setActive(false); entry.setDeactivatedAt(Instant.now()); } } }, 24 * 60 * 60 * 1000); // 24 hours } private String bytesToHex(byte[] bytes) { StringBuilder result = new StringBuilder(); for (byte b : bytes) { result.append(String.format("%02x", b)); } return result.toString(); } @SuppressWarnings("unchecked") private Map<String, Object> parseJson(String json) { try { com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); return mapper.readValue(json, Map.class); } catch (Exception e) { throw new RuntimeException("Failed to parse JSON", e); } } } class KeyEntry { private KeyPair keyPair; private String keyId; private String description; private Instant createdAt; private Instant deactivatedAt; private boolean active; private int usageCount; // Getters and setters public KeyPair getKeyPair() { return keyPair; } public void setKeyPair(KeyPair keyPair) { this.keyPair = keyPair; } public String getKeyId() { return keyId; } public void setKeyId(String keyId) { this.keyId = keyId; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public Instant getCreatedAt() { return createdAt; } public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; } public Instant getDeactivatedAt() { return deactivatedAt; } public void setDeactivatedAt(Instant deactivatedAt) { this.deactivatedAt = deactivatedAt; } public boolean isActive() { return active; } public void setActive(boolean active) { this.active = active; } public int getUsageCount() { return usageCount; } public void setUsageCount(int usageCount) { this.usageCount = usageCount; } public void incrementUsageCount() { this.usageCount++; } } class KeyStatistics { private int totalKeys; private int activeKeys; private int inactiveKeys; private java.time.Duration oldestKeyAge; // Getters and setters public int getTotalKeys() { return totalKeys; } public void setTotalKeys(int totalKeys) { this.totalKeys = totalKeys; } public int getActiveKeys() { return activeKeys; } public void setActiveKeys(int activeKeys) { this.activeKeys = activeKeys; } public int getInactiveKeys() { return inactiveKeys; } public void setInactiveKeys(int inactiveKeys) { this.inactiveKeys = inactiveKeys; } public java.time.Duration getOldestKeyAge() { return oldestKeyAge; } public void setOldestKeyAge(java.time.Duration oldestKeyAge) { this.oldestKeyAge = oldestKeyAge; } @Override public String toString() { return String.format( "KeyStatistics{total=%d, active=%d, inactive=%d, oldestAge=%s}", totalKeys, activeKeys, inactiveKeys, oldestKeyAge ); } } Example 5: Complete Usage Example
package com.jwt.eddsa; import java.security.KeyPair; import java.security.GeneralSecurityException; import java.util.*; public class EdDSAJwtDemo { public static void main(String[] args) { try { // Initialize Bouncy Castle Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); // Demo the complete flow demoKeyGeneration(); demoJwtCreationAndValidation(); demoAdvancedFeatures(); demoKeyRotation(); } catch (Exception e) { e.printStackTrace(); } } private static void demoKeyGeneration() throws GeneralSecurityException { System.out.println("=== EdDSA Key Generation Demo ==="); // Generate Ed25519 key pair KeyPairStrings ed25519Keys = EdDSAKeyGenerator.generateKeyPairStrings(EdDSAKeyGenerator.ED25519); System.out.println("Ed25519 Public Key: " + ed25519Keys.getPublicKey()); System.out.println("Ed25519 Private Key: " + ed25519Keys.getPrivateKey()); // Generate key pair object KeyPair keyPair = EdDSAKeyGenerator.generateKeyPair(EdDSAKeyGenerator.ED25519); KeyInfo keyInfo = EdDSAKeyGenerator.getKeyInfo(keyPair); System.out.println("Key Info: " + keyInfo); System.out.println(); } private static void demoJwtCreationAndValidation() throws Exception { System.out.println("=== JWT Creation and Validation Demo ==="); // Generate key pair KeyPair keyPair = EdDSAKeyGenerator.generateKeyPair(EdDSAKeyGenerator.ED25519); // Create JWT service EdDSAJwtService jwtService = new EdDSAJwtService("my-app", 3600000, 86400000); // Create claims Map<String, Object> claims = new HashMap<>(); claims.put("user_id", "12345"); claims.put("roles", Arrays.asList("USER", "ADMIN")); claims.put("email", "[email protected]"); // Create access token String accessToken = jwtService.createAccessToken(keyPair, "user123", claims); System.out.println("Access Token: " + accessToken); // Create refresh token String refreshToken = jwtService.createRefreshToken(keyPair, "user123"); System.out.println("Refresh Token: " + refreshToken); // Validate token JwtClaims parsedClaims = jwtService.parseToken(accessToken, keyPair.getPublic()); System.out.println("Parsed Subject: " + parsedClaims.getSubject()); System.out.println("Parsed Claims: " + parsedClaims.getClaims()); // Validate token quickly boolean isValid = jwtService.validateToken(accessToken, keyPair.getPublic()); System.out.println("Token is valid: " + isValid); System.out.println(); } private static void demoAdvancedFeatures() throws Exception { System.out.println("=== Advanced Features Demo ==="); KeyPair keyPair = EdDSAKeyGenerator.generateKeyPair(EdDSAKeyGenerator.ED25519); EdDSAJwtService basicJwtService = new EdDSAJwtService("my-app", 3600000, 86400000); AdvancedJwtService advancedJwtService = new AdvancedJwtService(basicJwtService); // Create advanced token request TokenRequest tokenRequest = new TokenRequest(); tokenRequest.setSubject("user123"); tokenRequest.setUserId("12345"); tokenRequest.setTokenType("access"); tokenRequest.setScopes(Arrays.asList("read", "write", "admin")); tokenRequest.setAudience("my-api"); tokenRequest.setRoles(Arrays.asList("USER", "ADMIN")); tokenRequest.setPermissions(Arrays.asList("users:read", "users:write")); DeviceContext deviceContext = new DeviceContext(); deviceContext.setDeviceId("device-001"); deviceContext.setTrustLevel("high"); tokenRequest.setDeviceContext(deviceContext); // Create advanced token String advancedToken = advancedJwtService.createAdvancedToken(keyPair, tokenRequest); System.out.println("Advanced Token: " + advancedToken); // Get token metadata TokenMetadata metadata = advancedJwtService.getTokenMetadata(advancedToken); System.out.println("Token Metadata: " + metadata.getAlgorithm() + ", Expires: " + metadata.getExpiration()); // Comprehensive validation ValidationContext validationContext = new ValidationContext(); validationContext.setExpectedIssuer("my-app"); validationContext.setExpectedAudience("my-api"); validationContext.setExpectedTokenType("access"); validationContext.setRequiredScopes(new HashSet<>(Arrays.asList("read", "write"))); TokenValidationResult validation = advancedJwtService.validateTokenComprehensive( advancedToken, keyPair.getPublic(), validationContext ); System.out.println("Validation Result: " + validation.isValid()); if (!validation.isValid()) { System.out.println("Validation Errors: " + validation.getErrorSummary()); } // Extract specific claim String userId = advancedJwtService.getClaim(advancedToken, keyPair.getPublic(), "user_id", String.class); System.out.println("Extracted User ID: " + userId); System.out.println(); } private static void demoKeyRotation() throws Exception { System.out.println("=== Key Rotation Demo ==="); // Create key manager KeyManager keyManager = new KeyManager(); // Get current key KeyPair currentKey = keyManager.getCurrentKeyPair(); System.out.println("Initial Key ID: " + keyManager.currentKeyId); // Create token with current key EdDSAJwtService jwtService = new EdDSAJwtService("my-app", 3600000, 86400000); String token = jwtService.createAccessToken(currentKey, "user123", null); System.out.println("Token with initial key: " + token); // Rotate key String newKeyId = keyManager.rotateKey(); System.out.println("New Key ID after rotation: " + newKeyId); // Validate token with key rotation support JwtClaims claims = keyManager.validateTokenWithKeyRotation(token); System.out.println("Validated token subject: " + claims.getSubject()); // Get key statistics KeyStatistics stats = keyManager.getKeyStatistics(); System.out.println("Key Statistics: " + stats); System.out.println(); } } Testing
Example 6: Comprehensive Tests
package com.jwt.eddsa; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import java.security.KeyPair; import java.security.Security; import java.util.HashMap; import java.util.Map; import static org.junit.jupiter.api.Assertions.*; class EdDSAJwtServiceTest { @BeforeAll static void setup() { Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); } @Test void testKeyGeneration() throws Exception { KeyPair keyPair = EdDSAKeyGenerator.generateKeyPair(EdDSAKeyGenerator.ED25519); assertNotNull(keyPair); assertNotNull(keyPair.getPublic()); assertNotNull(keyPair.getPrivate()); assertEquals("Ed25519", keyPair.getPublic().getAlgorithm()); } @Test void testJwtCreationAndValidation() throws Exception { KeyPair keyPair = EdDSAKeyGenerator.generateKeyPair(EdDSAKeyGenerator.ED25519); EdDSAJwtService jwtService = new EdDSAJwtService("test-issuer", 3600000, 86400000); Map<String, Object> claims = new HashMap<>(); claims.put("test_claim", "test_value"); String token = jwtService.createAccessToken(keyPair, "test-user", claims); assertNotNull(token); JwtClaims parsedClaims = jwtService.parseToken(token, keyPair.getPublic()); assertEquals("test-user", parsedClaims.getSubject()); assertEquals("test_value", parsedClaims.getClaims().get("test_claim")); } @Test void testTokenExpiration() throws Exception { KeyPair keyPair = EdDSAKeyGenerator.generateKeyPair(EdDSAKeyGenerator.ED25519); // Create service with very short expiration EdDSAJwtService jwtService = new EdDSAJwtService("test", 100, 1000); String token = jwtService.createAccessToken(keyPair, "test-user", null); // Wait for expiration Thread.sleep(200); assertThrows(JwtException.class, () -> { jwtService.parseToken(token, keyPair.getPublic()); }); } @Test void testInvalidSignature() throws Exception { KeyPair keyPair1 = EdDSAKeyGenerator.generateKeyPair(EdDSAKeyGenerator.ED25519); KeyPair keyPair2 = EdDSAKeyGenerator.generateKeyPair(EdDSAKeyGenerator.ED25519); EdDSAJwtService jwtService = new EdDSAJwtService("test", 3600000, 86400000); String token = jwtService.createAccessToken(keyPair1, "test-user", null); // Try to validate with wrong public key assertThrows(JwtException.class, () -> { jwtService.parseToken(token, keyPair2.getPublic()); }); } @Test void testKeyEncodingDecoding() throws Exception { KeyPair keyPair = EdDSAKeyGenerator.generateKeyPair(EdDSAKeyGenerator.ED25519); String encodedPublic = EdDSAKeyGenerator.encodePublicKey(keyPair.getPublic()); String encodedPrivate = EdDSAKeyGenerator.encodePrivateKey(keyPair.getPrivate()); assertNotNull(encodedPublic); assertNotNull(encodedPrivate); // Decode back KeyPair decodedKeyPair = new KeyPair( EdDSAKeyGenerator.decodePublicKey(encodedPublic, "Ed25519"), EdDSAKeyGenerator.decodePrivateKey(encodedPrivate, "Ed25519") ); assertEquals(keyPair.getPublic(), decodedKeyPair.getPublic()); assertEquals(keyPair.getPrivate(), decodedKeyPair.getPrivate()); } } Best Practices and Security Considerations
Security Best Practices
package com.jwt.eddsa; import java.security.SecureRandom; import java.util.Base64; public class SecurityBestPractices { /** * Secure random number generator for key generation */ public static SecureRandom createSecureRandom() { try { return SecureRandom.getInstanceStrong(); } catch (Exception e) { return new SecureRandom(); } } /** * Validate JWT header for security */ public static void validateJwtHeader(Map<String, Object> header) throws JwtException { // Check algorithm String alg = (String) header.get("alg"); if (!"Ed25519".equals(alg) && !"Ed448".equals(alg)) { throw new JwtException("Unsupported algorithm: " + alg); } // Check type String typ = (String) header.get("typ"); if (!"JWT".equals(typ)) { throw new JwtException("Unsupported token type: " + typ); } // Check for critical headers if (header.containsKey("crit")) { throw new JwtException("Critical headers not supported"); } } /** * Secure key storage recommendations */ public static class KeyStorage { /** * Encrypt private key for storage */ public static String encryptPrivateKey(byte[] privateKey, String password) { // In production, use proper encryption like AES-GCM // This is a simplified example byte[] encrypted = performEncryption(privateKey, password); return Base64.getUrlEncoder().withoutPadding().encodeToString(encrypted); } /** * Decrypt private key from storage */ public static byte[] decryptPrivateKey(String encryptedKey, String password) { byte[] encryptedBytes = Base64.getUrlDecoder().decode(encryptedKey); return performDecryption(encryptedBytes, password); } private static byte[] performEncryption(byte[] data, String password) { // Implement proper encryption in production return data; // Placeholder } private static byte[] performDecryption(byte[] data, String password) { // Implement proper decryption in production return data; // Placeholder } } } Conclusion
This comprehensive EdDSA JWT implementation provides:
Key Features:
- EdDSA Key Generation: Support for Ed25519 and Ed448
- JWT Creation & Validation: Complete JWT lifecycle management
- Advanced Claims: Support for custom claims and validation
- Key Rotation: Secure key management and rotation
- Security: Comprehensive security best practices
Benefits of EdDSA for JWT:
- Performance: Faster signing and verification
- Security: Strong security guarantees
- Compact: Smaller key and signature sizes
- Deterministic: No need for random number generation
Production Considerations:
- Key Storage: Use HSMs or secure key management systems
- Key Rotation: Implement regular key rotation policies
- Audit Logging: Log all token creation and validation events
- Monitoring: Monitor token usage patterns and failures
- Revocation: Implement token revocation mechanisms when needed
This implementation provides a solid foundation for using EdDSA with JWT in Java applications, offering both security and performance benefits over traditional algorithms like RSA and ECDSA.