JWT Signing with EdDSA in Java: Complete Implementation Guide

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:

  1. EdDSA Key Generation: Support for Ed25519 and Ed448
  2. JWT Creation & Validation: Complete JWT lifecycle management
  3. Advanced Claims: Support for custom claims and validation
  4. Key Rotation: Secure key management and rotation
  5. 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.

Leave a Reply

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


Macro Nepal Helper