JOSE (JWT, JWS, JWE) in Java: Complete Guide to JSON Object Signing and Encryption

In modern distributed systems, the need for secure, interoperable token formats has never been greater. JSON Object Signing and Encryption (JOSE) is a suite of specifications that provides a standardized way to represent claims, sign them, and encrypt them using JSON data structures. For Java developers building microservices, authentication systems, or any application requiring secure token exchange, understanding and correctly implementing JOSE—including JWT, JWS, and JWE—is essential.

What is JOSE?

JOSE (JSON Object Signing and Encryption) is a framework of specifications for securely transferring claims between parties. It consists of several interrelated standards:

SpecificationPurposeRFC
JWT (JSON Web Token)Represents claims as a JSON objectRFC 7519
JWS (JSON Web Signature)Signs content (JWT or arbitrary data)RFC 7515
JWE (JSON Web Encryption)Encrypts content (JWT or arbitrary data)RFC 7516
JWK (JSON Web Key)Represents cryptographic keys in JSONRFC 7517
JWA (JSON Web Algorithms)Defines cryptographic algorithmsRFC 7518

Why JOSE is Essential for Java Applications

  1. Interoperability: Standardized format works across different platforms and languages.
  2. Stateless Authentication: JWT enables distributed authentication without server-side session storage.
  3. Flexible Security: Choose between signed only (JWS) or encrypted (JWE) tokens.
  4. Standard Algorithms: Well-defined cryptographic algorithms with clear security properties.
  5. Claims-Based: Rich claim structure for expressing user attributes and permissions.
  6. Microservices Ready: Perfect for service-to-service authentication and authorization.

Java Libraries for JOSE

1. Maven Dependencies

<dependencies>
<!-- Nimbus JOSE + JWT (Most popular, actively maintained) -->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.37.3</version>
</dependency>
<!-- JJWT (Simple, opinionated, Java JWT library) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
<!-- Spring Security OAuth2 Resource Server -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<version>3.2.0</version>
</dependency>
<!-- Bouncy Castle for advanced algorithms -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.78</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>1.78</version>
</dependency>
</dependencies>

JSON Web Tokens (JWT) Structure

A JWT consists of three parts separated by dots:

header.payload.signature

Header:

{
"alg": "HS256",
"typ": "JWT"
}

Payload (Claims):

{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622,
"roles": ["admin", "user"]
}

Signature: Cryptographic signature of header + payload

JSON Web Signature (JWS) Implementation

1. Basic JWT Signing and Verification with Nimbus JOSE

import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.*;
import com.nimbusds.jwt.*;
import java.security.SecureRandom;
import java.util.*;
import java.time.Instant;
public class JWSExample {
public static void main(String[] args) throws Exception {
System.out.println("=== JWS (JSON Web Signature) Examples ===\n");
// 1. HMAC SHA-256 (Symmetric)
hmacExample();
// 2. RSA SHA-256 (Asymmetric)
rsaExample();
// 3. ECDSA (Elliptic Curve)
ecdsaExample();
// 4. Ed25519 (Edwards-curve Digital Signature Algorithm)
ed25519Example();
}
public static void hmacExample() throws Exception {
System.out.println("\n--- HMAC SHA-256 (HS256) ---");
// Generate a secure secret key (at least 256 bits)
byte[] secret = new byte[32];
new SecureRandom().nextBytes(secret);
// Create signer
JWSSigner signer = new MACSigner(secret);
// Build JWT claims
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.subject("alice")
.issuer("https://issuer.example.com")
.claim("name", "Alice Smith")
.claim("roles", Arrays.asList("user", "admin"))
.issueTime(new Date())
.expirationTime(Date.from(Instant.now().plusSeconds(3600)))
.jwtID(UUID.randomUUID().toString())
.build();
// Create JWS object
SignedJWT signedJWT = new SignedJWT(
new JWSHeader(JWSAlgorithm.HS256),
claimsSet
);
// Sign
signedJWT.sign(signer);
// Serialize
String token = signedJWT.serialize();
System.out.println("JWT Token: " + token);
System.out.println("Token length: " + token.length() + " characters");
// Verify
JWSVerifier verifier = new MACVerifier(secret);
boolean verified = signedJWT.verify(verifier);
System.out.println("Signature verified: " + verified);
// Extract claims
JWTClaimsSet extracted = signedJWT.getJWTClaimsSet();
System.out.println("Subject: " + extracted.getSubject());
System.out.println("Issuer: " + extracted.getIssuer());
System.out.println("Roles: " + extracted.getStringListClaim("roles"));
}
public static void rsaExample() throws Exception {
System.out.println("\n--- RSA SHA-256 (RS256) ---");
// Generate RSA key pair
java.security.KeyPairGenerator keyGen = 
java.security.KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
java.security.KeyPair keyPair = keyGen.generateKeyPair();
// Create signer with private key
JWSSigner signer = new RSASSASigner(keyPair.getPrivate());
// Build claims
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.subject("bob")
.issuer("https://auth.example.com")
.claim("email", "[email protected]")
.issueTime(new Date())
.build();
// Sign
SignedJWT signedJWT = new SignedJWT(
new JWSHeader.Builder(JWSAlgorithm.RS256)
.keyID("rsa-key-1")
.build(),
claimsSet
);
signedJWT.sign(signer);
String token = signedJWT.serialize();
System.out.println("RSA JWT: " + token.substring(0, 50) + "...");
// Verify with public key
JWSVerifier verifier = new RSASSAVerifier((java.security.interfaces.RSAPublicKey) 
keyPair.getPublic());
boolean verified = signedJWT.verify(verifier);
System.out.println("RSA signature verified: " + verified);
}
public static void ecdsaExample() throws Exception {
System.out.println("\n--- ECDSA (ES256) ---");
// Generate EC key pair (P-256 curve)
java.security.KeyPairGenerator keyGen = 
java.security.KeyPairGenerator.getInstance("EC");
keyGen.initialize(new java.security.spec.ECGenParameterSpec("secp256r1"));
java.security.KeyPair keyPair = keyGen.generateKeyPair();
// Create signer
JWSSigner signer = new ECDSASigner(keyPair.getPrivate());
// Build and sign
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.subject("charlie")
.claim("scope", "read write")
.build();
SignedJWT signedJWT = new SignedJWT(
new JWSHeader(JWSAlgorithm.ES256),
claimsSet
);
signedJWT.sign(signer);
String token = signedJWT.serialize();
System.out.println("ECDSA JWT: " + token.substring(0, 50) + "...");
// Verify
JWSVerifier verifier = new ECDSAVerifier(
(java.security.interfaces.ECPublicKey) keyPair.getPublic());
boolean verified = signedJWT.verify(verifier);
System.out.println("ECDSA verified: " + verified);
}
public static void ed25519Example() throws Exception {
System.out.println("\n--- Ed25519 (EdDSA) ---");
// Note: Requires Bouncy Castle
java.security.Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
try {
java.security.KeyPairGenerator keyGen = 
java.security.KeyPairGenerator.getInstance("Ed25519", "BC");
java.security.KeyPair keyPair = keyGen.generateKeyPair();
// Create signer
JWSSigner signer = new Ed25519Signer(keyPair.getPrivate());
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.subject("david")
.build();
SignedJWT signedJWT = new SignedJWT(
new JWSHeader(JWSAlgorithm.EdDSA),
claimsSet
);
signedJWT.sign(signer);
String token = signedJWT.serialize();
System.out.println("Ed25519 JWT: " + token.substring(0, 50) + "...");
// Verify
JWSVerifier verifier = new Ed25519Verifier(
(java.security.PublicKey) keyPair.getPublic());
boolean verified = signedJWT.verify(verifier);
System.out.println("Ed25519 verified: " + verified);
} catch (Exception e) {
System.out.println("Ed25519 not available: " + e.getMessage());
}
}
}

2. JWT with JJWT Library (Simpler API)

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
import java.security.Key;
import java.util.Date;
import java.util.Map;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
public class JJWTExample {
public static void main(String[] args) {
System.out.println("=== JJWT Library Examples ===\n");
// 1. Symmetric signing (HS256)
symmetricExample();
// 2. Asymmetric signing (RS256)
asymmetricExample();
// 3. JWT parsing and validation
parsingExample();
// 4. JWT with custom claims
customClaimsExample();
}
public static void symmetricExample() {
System.out.println("\n--- HMAC Symmetric (HS256) ---");
// Generate a secure key for HS256
SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
// Create JWT
String jws = Jwts.builder()
.subject("alice")
.issuer("https://myapp.com")
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + 3600000)) // 1 hour
.claim("name", "Alice Wonderland")
.claim("roles", new String[]{"user", "admin"})
.signWith(key)
.compact();
System.out.println("JWT: " + jws);
System.out.println("Parts: " + jws.split("\\.").length);
// Parse and verify
Jws<Claims> jwsClaims = Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(jws);
Claims claims = jwsClaims.getPayload();
System.out.println("Subject: " + claims.getSubject());
System.out.println("Name: " + claims.get("name", String.class));
System.out.println("Roles: " + String.join(", ", claims.get("roles", String[].class)));
}
public static void asymmetricExample() {
System.out.println("\n--- RSA Asymmetric (RS256) ---");
// Generate RSA key pair
java.security.KeyPair keyPair = Keys.keyPairFor(SignatureAlgorithm.RS256);
// Sign with private key
String jws = Jwts.builder()
.subject("bob")
.issuer("auth-server")
.claim("scope", "read:profile")
.signWith(keyPair.getPrivate())
.compact();
System.out.println("RSA JWT: " + jws);
// Verify with public key
Jws<Claims> jwsClaims = Jwts.parser()
.verifyWith(keyPair.getPublic())
.build()
.parseSignedClaims(jws);
boolean valid = jwsClaims != null;
System.out.println("Signature valid: " + valid);
}
public static void parsingExample() {
System.out.println("\n--- JWT Parsing and Validation ---");
SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
// Create expired token
String expiredJwt = Jwts.builder()
.subject("test")
.expiration(new Date(System.currentTimeMillis() - 1000)) // Already expired
.signWith(key)
.compact();
// Create valid token
String validJwt = Jwts.builder()
.subject("test")
.expiration(new Date(System.currentTimeMillis() + 3600000))
.signWith(key)
.compact();
// Parse valid token
try {
Jws<Claims> valid = Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(validJwt);
System.out.println("Valid token accepted");
} catch (JwtException e) {
System.out.println("Valid token rejected: " + e.getMessage());
}
// Parse expired token
try {
Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(expiredJwt);
System.out.println("ERROR: Expired token accepted");
} catch (ExpiredJwtException e) {
System.out.println("Expired token correctly rejected: " + e.getMessage());
}
}
public static void customClaimsExample() {
System.out.println("\n--- Custom Claims ---");
SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
// Create with custom claims
Map<String, Object> customClaims = new HashMap<>();
customClaims.put("tenant", "acme-corp");
customClaims.put("region", "us-east");
customClaims.put("feature_flags", new String[]{"beta", "early-access"});
String jws = Jwts.builder()
.subject("charlie")
.claims(customClaims)
.signWith(key)
.compact();
// Parse and extract
Jws<Claims> parsed = Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(jws);
Claims claims = parsed.getPayload();
System.out.println("Tenant: " + claims.get("tenant"));
System.out.println("Region: " + claims.get("region"));
System.out.println("Features: " + String.join(", ", 
claims.get("feature_flags", String[].class)));
}
}

JSON Web Encryption (JWE) Implementation

JWE provides confidentiality by encrypting the payload, optionally with additional authenticated data.

1. Basic JWE with Nimbus

import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.*;
import com.nimbusds.jwt.*;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
public class JWEExample {
public static void main(String[] args) throws Exception {
System.out.println("=== JWE (JSON Web Encryption) Examples ===\n");
// 1. Direct encryption with AES-GCM
directEncryptionExample();
// 2. Key encryption with RSA-OAEP
rsaKeyEncryptionExample();
// 3. Password-based encryption
passwordBasedExample();
// 4. Nested JWS + JWE (Sign then Encrypt)
nestedExample();
}
public static void directEncryptionExample() throws Exception {
System.out.println("\n--- Direct Encryption (A256GCM) ---");
// Generate AES key for content encryption
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256);
SecretKey aesKey = keyGen.generateKey();
// Create JWE header
JWEHeader header = new JWEHeader.Builder(JWEAlgorithm.DIR, EncryptionMethod.A256GCM)
.contentType("JWT") // Nested JWT
.build();
// Create claims
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.subject("sensitive-data")
.claim("credit_card", "4111-1111-1111-1111")
.claim("cvv", "123")
.expirationTime(new Date(System.currentTimeMillis() + 300000)) // 5 minutes
.build();
// Create JWE
Payload payload = new Payload(claimsSet.toJSONObject());
JWEObject jweObject = new JWEObject(header, payload);
// Encrypt
jweObject.encrypt(new DirectEncrypter(aesKey));
// Serialize
String jweString = jweObject.serialize();
System.out.println("JWE Token: " + jweString);
System.out.println("JWE Parts: " + jweString.split("\\.").length);
// Decrypt
JWEObject parsed = JWEObject.parse(jweString);
parsed.decrypt(new DirectDecrypter(aesKey));
JWTClaimsSet decrypted = JWTClaimsSet.parse(parsed.getPayload().toJSONObject());
System.out.println("Decrypted subject: " + decrypted.getSubject());
System.out.println("Decrypted credit card: " + decrypted.getClaim("credit_card"));
}
public static void rsaKeyEncryptionExample() throws Exception {
System.out.println("\n--- RSA Key Encryption (RSA-OAEP + A256GCM) ---");
// Generate RSA key pair for key encryption
KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("RSA");
rsaGen.initialize(2048);
KeyPair rsaKeyPair = rsaGen.generateKeyPair();
// Create JWE with RSA key encryption
JWEHeader header = new JWEHeader.Builder(
JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM)
.keyID("rsa-encryption-key")
.build();
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.subject("encrypted-payload")
.claim("secret", "TopSecretInformation")
.build();
Payload payload = new Payload(claimsSet.toJSONObject());
JWEObject jweObject = new JWEObject(header, payload);
// Encrypt with RSA public key
jweObject.encrypt(new RSAEncrypter(rsaKeyPair.getPublic()));
String encrypted = jweObject.serialize();
System.out.println("RSA Encrypted JWE: " + encrypted.substring(0, 60) + "...");
// Decrypt with RSA private key
JWEObject parsed = JWEObject.parse(encrypted);
parsed.decrypt(new RSADecrypter(rsaKeyPair.getPrivate()));
JWTClaimsSet decrypted = JWTClaimsSet.parse(parsed.getPayload().toJSONObject());
System.out.println("Decrypted: " + decrypted.getClaim("secret"));
}
public static void passwordBasedExample() throws Exception {
System.out.println("\n--- Password-Based Encryption (PBES2) ---");
String password = "user-password";
// Create header with PBES2
JWEHeader header = new JWEHeader.Builder(
JWEAlgorithm.PBES2_HS256_A128KW, EncryptionMethod.A128GCM)
.pbes2SaltInput(Base64URL.encode("salt".getBytes()))
.pbes2Count(4096) // Iteration count
.build();
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.claim("encrypted-by", "password")
.build();
Payload payload = new Payload(claimsSet.toJSONObject());
JWEObject jweObject = new JWEObject(header, payload);
// Encrypt with password
jweObject.encrypt(new PasswordBasedEncrypter(password, 4096));
String encrypted = jweObject.serialize();
System.out.println("Password encrypted: " + encrypted);
// Decrypt with password
JWEObject parsed = JWEObject.parse(encrypted);
parsed.decrypt(new PasswordBasedDecrypter(password));
System.out.println("Successfully decrypted with password");
}
public static void nestedExample() throws Exception {
System.out.println("\n--- Nested JWS + JWE (Sign then Encrypt) ---");
// Setup keys
KeyGenerator aesGen = KeyGenerator.getInstance("AES");
aesGen.init(256);
SecretKey encryptionKey = aesGen.generateKey();
KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("RSA");
rsaGen.initialize(2048);
KeyPair signingKeyPair = rsaGen.generateKeyPair();
// Step 1: Create and sign inner JWT
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.subject("alice")
.claim("data", "sensitive-information")
.build();
SignedJWT signedJWT = new SignedJWT(
new JWSHeader(JWSAlgorithm.RS256),
claimsSet
);
signedJWT.sign(new RSASSASigner(signingKeyPair.getPrivate()));
// Step 2: Encrypt the signed JWT
JWEHeader jweHeader = new JWEHeader.Builder(
JWEAlgorithm.DIR, EncryptionMethod.A256GCM)
.contentType("JWT") // Indicates nested JWT
.build();
Payload payload = new Payload(signedJWT.serialize());
JWEObject jweObject = new JWEObject(jweHeader, payload);
jweObject.encrypt(new DirectEncrypter(encryptionKey));
String nestedToken = jweObject.serialize();
System.out.println("Nested (Encrypted + Signed) token: " + nestedToken);
// Step 3: Decrypt and verify
JWEObject parsedJWE = JWEObject.parse(nestedToken);
parsedJWE.decrypt(new DirectDecrypter(encryptionKey));
SignedJWT parsedSigned = SignedJWT.parse(parsedJWE.getPayload().toString());
parsedSigned.verify(new RSASSAVerifier((java.security.interfaces.RSAPublicKey) 
signingKeyPair.getPublic()));
JWTClaimsSet extracted = parsedSigned.getJWTClaimsSet();
System.out.println("Decrypted and verified subject: " + extracted.getSubject());
}
}

JSON Web Keys (JWK) Management

1. JWK Generation and Serialization

import com.nimbusds.jose.jwk.*;
import com.nimbusds.jose.jwk.gen.*;
import com.nimbusds.jose.jwk.source.*;
import com.nimbusds.jose.proc.SecurityContext;
import java.util.*;
public class JWKExample {
public static void main(String[] args) throws Exception {
System.out.println("=== JWK (JSON Web Key) Examples ===\n");
// 1. Generate RSA JWK
rsaJWKExample();
// 2. Generate EC JWK
ecJWKExample();
// 3. Generate Octet (Symmetric) JWK
octetJWKExample();
// 4. JWK Set (JWKS)
jwksExample();
// 5. JWK Rotation
keyRotationExample();
}
public static void rsaJWKExample() throws Exception {
System.out.println("\n--- RSA JWK ---");
// Generate RSA key pair as JWK
RSAKey rsaJWK = new RSAKeyGenerator(RSAKeyLength.LEN_2048)
.keyID("rsa-1")
.algorithm(JWSAlgorithm.RS256)
.keyUse(KeyUse.SIGNATURE)
.generate();
// Get public and private JWK
RSAKey publicJWK = rsaJWK.toPublicJWK();
// Convert to JSON
String jsonString = rsaJWK.toJSONString();
System.out.println("RSA JWK (private): " + jsonString);
System.out.println("RSA JWK (public): " + publicJWK.toJSONString());
// Parse back from JSON
RSAKey parsed = RSAKey.parse(jsonString);
System.out.println("Parsed key ID: " + parsed.getKeyID());
}
public static void ecJWKExample() throws Exception {
System.out.println("\n--- EC JWK ---");
// Generate EC key pair (P-256)
ECKey ecJWK = new ECKeyGenerator(Curve.P_256)
.keyID("ec-1")
.keyUse(KeyUse.SIGNATURE)
.algorithm(JWSAlgorithm.ES256)
.generate();
System.out.println("EC JWK: " + ecJWK.toJSONString());
System.out.println("Curve: " + ecJWK.getCurve());
// Get public key
ECKey publicJWK = ecJWK.toPublicJWK();
System.out.println("Public EC JWK: " + publicJWK.toJSONString());
}
public static void octetJWKExample() throws Exception {
System.out.println("\n--- Octet (Symmetric) JWK ---");
// Generate symmetric key for HMAC
OctetSequenceKey hmacKey = new OctetSequenceKeyGenerator(256)
.keyID("hmac-1")
.algorithm(JWSAlgorithm.HS256)
.keyUse(KeyUse.SIGNATURE)
.generate();
System.out.println("HMAC JWK: " + hmacKey.toJSONString());
// Generate symmetric key for AES encryption
OctetSequenceKey aesKey = new OctetSequenceKeyGenerator(256)
.keyID("aes-1")
.algorithm(EncryptionMethod.A256GCM.getName())
.keyUse(KeyUse.ENCRYPTION)
.generate();
System.out.println("AES JWK: " + aesKey.toJSONString());
}
public static void jwksExample() throws Exception {
System.out.println("\n--- JWK Set (JWKS) ---");
// Create multiple keys
RSAKey rsaKey = new RSAKeyGenerator(RSAKeyLength.LEN_2048)
.keyID("sig-1")
.algorithm(JWSAlgorithm.RS256)
.keyUse(KeyUse.SIGNATURE)
.generate()
.toPublicJWK();
ECKey ecKey = new ECKeyGenerator(Curve.P_256)
.keyID("sig-2")
.algorithm(JWSAlgorithm.ES256)
.keyUse(KeyUse.SIGNATURE)
.generate()
.toPublicJWK();
OctetSequenceKey hmacKey = new OctetSequenceKeyGenerator(256)
.keyID("sig-3")
.algorithm(JWSAlgorithm.HS256)
.keyUse(KeyUse.SIGNATURE)
.generate()
.toPublicJWK();
// Create JWK set
JWKSet jwkSet = new JWKSet(Arrays.asList(rsaKey, ecKey, hmacKey));
String jwksJson = jwkSet.toJSONObject(true).toJSONString();
System.out.println("JWKS: " + jwksJson);
// Parse back
JWKSet parsed = JWKSet.parse(jwksJson);
System.out.println("Parsed key count: " + parsed.getKeys().size());
// Get key by ID
JWK key = parsed.getKeyByKeyId("sig-1");
System.out.println("Found key: " + (key != null));
}
public static void keyRotationExample() throws Exception {
System.out.println("\n--- Key Rotation ---");
// Current active key
RSAKey activeKey = new RSAKeyGenerator(RSAKeyLength.LEN_2048)
.keyID("key-2024-01")
.issueTime(new Date())
.build();
// Generate new key for rotation
RSAKey newKey = new RSAKeyGenerator(RSAKeyLength.LEN_2048)
.keyID("key-2024-02")
.issueTime(new Date())
.build();
// JWKS with both keys (clients can use either)
JWKSet jwkSet = new JWKSet(Arrays.asList(
activeKey.toPublicJWK(),
newKey.toPublicJWK()
));
System.out.println("Rotation JWKS: " + jwkSet.toJSONObject(true).toJSONString());
// Simulate token signed with old key
JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256)
.keyID("key-2024-01")
.build();
System.out.println("Token signed with old key (keyID: " + header.getKeyID() + ")");
// Find appropriate key for verification
JWK matchingKey = jwkSet.getKeyByKeyId(header.getKeyID());
System.out.println("Found matching key: " + (matchingKey != null));
}
}

Spring Boot Integration

1. Resource Server Configuration

@Configuration
@EnableWebSecurity
public class ResourceServerConfig {
@Value("${jwt.public-key}")
private String publicKeyPem;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**").permitAll()
.requestMatchers("/api/user").hasAuthority("SCOPE_read")
.requestMatchers("/api/admin").hasAuthority("SCOPE_admin")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.decoder(jwtDecoder())
)
);
return http.build();
}
@Bean
public JwtDecoder jwtDecoder() {
// Parse PEM public key
try {
String pem = publicKeyPem
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s", "");
byte[] encoded = Base64.getDecoder().decode(pem);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(encoded);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(keySpec);
return NimbusJwtDecoder.withPublicKey((RSAPublicKey) publicKey).build();
} catch (Exception e) {
throw new RuntimeException("Failed to configure JWT decoder", e);
}
}
}

2. Token Service Implementation

@Service
public class TokenService {
private final JWKSource<SecurityContext> jwkSource;
private final JwtEncoder jwtEncoder;
public TokenService() throws Exception {
// Generate RSA key pair for signing
RSAKey rsaKey = new RSAKeyGenerator(2048)
.keyID(UUID.randomUUID().toString())
.algorithm(JWSAlgorithm.RS256)
.generate();
this.jwkSource = new ImmutableJWKSet<>(new JWKSet(rsaKey));
this.jwtEncoder = new NimbusJwtEncoder(jwkSource);
}
public String generateToken(String subject, List<String> roles, Duration ttl) {
Instant now = Instant.now();
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("https://auth.example.com")
.subject(subject)
.issuedAt(now)
.expiresAt(now.plus(ttl))
.claim("roles", roles)
.claim("scope", String.join(" ", roles))
.build();
JwsHeader header = JwsHeader.with(JwsAlgorithm.RS256)
.keyId(jwkSource.get(new JWKSelector(new JWKMatcher.Builder()
.keyType(KeyType.RSA)
.keyUse(KeyUse.SIGNATURE)
.build()), null)
.get(0).getKeyID())
.build();
return this.jwtEncoder.encode(JwtEncoderParameters.from(header, claims))
.getTokenValue();
}
@RestController
@RequestMapping("/api")
public static class TokenController {
@Autowired
private TokenService tokenService;
@PostMapping("/auth/login")
public ResponseEntity<Map<String, String>> login(@RequestBody LoginRequest request) {
// Authenticate user (simplified)
if ("user".equals(request.username()) && "pass".equals(request.password())) {
String token = tokenService.generateToken(
request.username(),
List.of("read"),
Duration.ofHours(1)
);
return ResponseEntity.ok(Map.of(
"access_token", token,
"token_type", "Bearer",
"expires_in", "3600"
));
}
return ResponseEntity.status(401).build();
}
@GetMapping("/user/profile")
@PreAuthorize("hasAuthority('SCOPE_read')")
public UserProfile getProfile(JwtAuthenticationToken token) {
String username = token.getToken().getSubject();
return new UserProfile(username, "[email protected]");
}
}
record LoginRequest(String username, String password) {}
record UserProfile(String username, String email) {}
}

3. Custom JWT Validator

@Component
public class CustomJwtValidator {
private final String issuer;
private final String audience;
private final JWKSet jwkSet;
public CustomJwtValidator(@Value("${jwt.issuer}") String issuer,
@Value("${jwt.audience}") String audience,
@Value("${jwt.jwks-url}") String jwksUrl) throws Exception {
this.issuer = issuer;
this.audience = audience;
// Load JWKS from URL
this.jwkSet = JWKSet.load(new URL(jwksUrl));
}
public Claims validateToken(String token) throws Exception {
SignedJWT signedJWT = SignedJWT.parse(token);
// Validate signature
JWK jwk = jwkSet.getKeyByKeyId(signedJWT.getHeader().getKeyID());
if (jwk == null) {
throw new SecurityException("Key not found");
}
// Create verifier based on key type
JWSVerifier verifier;
if (jwk instanceof RSAKey rsaKey) {
verifier = new RSASSAVerifier(rsaKey.toRSAPublicKey());
} else if (jwk instanceof ECKey ecKey) {
verifier = new ECDSAVerifier(ecKey.toECPublicKey());
} else {
throw new SecurityException("Unsupported key type");
}
if (!signedJWT.verify(verifier)) {
throw new SecurityException("Invalid signature");
}
// Validate claims
JWTClaimsSet claims = signedJWT.getJWTClaimsSet();
// Check expiration
Date now = new Date();
if (claims.getExpirationTime() == null || claims.getExpirationTime().before(now)) {
throw new SecurityException("Token expired");
}
// Check issuer
if (!issuer.equals(claims.getIssuer())) {
throw new SecurityException("Invalid issuer");
}
// Check audience
if (claims.getAudience() != null && 
!claims.getAudience().contains(audience)) {
throw new SecurityException("Invalid audience");
}
return new Claims(claims);
}
public static class Claims {
private final JWTClaimsSet claims;
Claims(JWTClaimsSet claims) {
this.claims = claims;
}
public String getSubject() {
return claims.getSubject();
}
public List<String> getRoles() {
return claims.getStringListClaim("roles");
}
public Map<String, Object> getAll() {
return claims.getClaims();
}
}
}

Security Best Practices

  1. Algorithm Selection:
    • Prefer RS256, ES256, or EdDSA over HS256 for distributed systems
    • Avoid "none" algorithm entirely
    • Use RSA-OAEP or ECDH-ES for key encryption in JWE
  2. Key Management:
    • Rotate keys regularly
    • Use appropriate key sizes (RSA 2048+, EC P-256+)
    • Store private keys securely (HSM, KMS, secure vaults)
  3. Token Validation:
    • Always validate signature
    • Check expiration time
    • Verify issuer and audience
    • Validate not-before time
  4. Claims Protection:
    • Don't store sensitive data in JWT unless encrypted
    • Use JWE for confidential claims
    • Keep token size reasonable
  5. Error Handling:
    • Don't leak details about validation failures
    • Use generic error messages
    • Log security events appropriately

Conclusion

JOSE, encompassing JWT, JWS, and JWE, provides Java developers with a comprehensive, standards-based framework for secure token handling. Whether building authentication systems, microservice authorization, or secure data exchange, these specifications offer the flexibility and security required for modern applications.

By leveraging mature Java libraries like Nimbus JOSE + JWT and JJWT, developers can implement these standards correctly while focusing on their application's specific requirements. The combination of JWS for integrity and JWE for confidentiality, along with JWK for key management, creates a complete ecosystem for secure token-based communication that's interoperable across platforms and programming languages.

Java Programming Intermediate Topics – Modifiers, Loops, Math, Methods & Projects (Related to Java Programming)


Access Modifiers in Java:
Access modifiers control how classes, variables, and methods are accessed from different parts of a program. Java provides four main access levels—public, private, protected, and default—which help protect data and control visibility in object-oriented programming.
Read more: https://macronepal.com/blog/access-modifiers-in-java-a-complete-guide/


Static Variables in Java:
Static variables belong to the class rather than individual objects. They are shared among all instances of the class and are useful for storing values that remain common across multiple objects.
Read more: https://macronepal.com/blog/static-variables-in-java-a-complete-guide/


Method Parameters in Java:
Method parameters allow values to be passed into methods so that operations can be performed using supplied data. They help make methods flexible and reusable in different parts of a program.
Read more: https://macronepal.com/blog/method-parameters-in-java-a-complete-guide/


Random Numbers in Java:
This topic explains how to generate random numbers in Java for tasks such as simulations, games, and random selections. Random numbers help create unpredictable results in programs.
Read more: https://macronepal.com/blog/random-numbers-in-java-a-complete-guide/


Math Class in Java:
The Math class provides built-in methods for performing mathematical calculations such as powers, square roots, rounding, and other advanced calculations used in Java programs.
Read more: https://macronepal.com/blog/math-class-in-java-a-complete-guide/


Boolean Operations in Java:
Boolean operations use true and false values to perform logical comparisons. They are commonly used in conditions and decision-making statements to control program flow.
Read more: https://macronepal.com/blog/boolean-operations-in-java-a-complete-guide/


Nested Loops in Java:
Nested loops are loops placed inside other loops to perform repeated operations within repeated tasks. They are useful for pattern printing, tables, and working with multi-level data.
Read more: https://macronepal.com/blog/nested-loops-in-java-a-complete-guide/


Do-While Loop in Java:
The do-while loop allows a block of code to run at least once before checking the condition. It is useful when the program must execute a task before verifying whether it should continue.
Read more: https://macronepal.com/blog/do-while-loop-in-java-a-complete-guide/


Simple Calculator Project in Java:
This project demonstrates how to create a basic calculator program using Java. It combines input handling, arithmetic operations, and conditional logic to perform simple mathematical calculations.
Read more: https://macronepal.com/blog/simple-calculator-project-in-java/


Leave a Reply

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


Macro Nepal Helper