JWT, JWS, JWE

Wed. Jun 24th, 2026 07:45 PM9 mins read
JWT, JWS, JWE
Source: Gemini Nano Banana Pro - jwt

JWT (JSON Web Token) adalah standar yang digunakan untuk komunikasi antara client & server pada proses Authentication & Authorization. JWT punya 2 pilihan token. Pertama JWS (JSON Web Signature), yaitu informasi yang ada di token di-encode menggunakan Base64. Kedua JWE (JSON Web Encryption), yaitu informasi yang ada di token di-encrypt dengan algoritma tertentu. Cara generate dan verifikasinya pun ada 2, bisa menggunakan Secret Key yang sama, bisa juga menggunakan pasangan Public Key & Private Key. JWT ini stateless karena untuk verifikasi data bisa langsung dari payload yang ada di dalamnya tanpa perlu query ke database.

JWT di Java

Pada Java kita bisa menggunakan library seperti jjwt untuk Authorization. Menurut gw ini library yang cukup simple dan banyak digunakan.

pom.xml
Copy
<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt-api</artifactId>
	<version>${jjwt-api.version}</version>
</dependency>
<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt-impl</artifactId>
	<version>${jjwt-api.version}</version>
	<scope>runtime</scope>
</dependency>
<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt-jackson</artifactId>
	<version>${jjwt-api.version}</version>
	<scope>runtime</scope>
</dependency>

JWS

JWS adalah jenis JWT yang paling populer. Token JWS terdiri dari 3 bagian yang dipisahkan oleh simbol titik (.), yaitu Header, Payload, dan Signature:

  • Header: berisi info algoritma yang dipakai, tipe JWT, dan hal-hal teknis lainnya yang di-encode;
  • Payload: berisi data yang di-encode, seperti waktu expire (exp), penerbit token (iss), target client (aud), user roles, scopes, serta info-info penting terkait user yang bisa di-custom sesuai kebutuhan untuk verifikasi yang bukan data sensitif karena ini hal yang bisa dibaca;
  • Signature: adalah hash menggunakan key dari header & payload sehingga value dari JWT ini ga bisa diubah dari luar selama key yang digunakan ga bocor;
Copy
${Base64(header)}.${Base64(payload)}.${Base64(hash(header + payload + key))}

Setelah berhasil Authentication, nanti server akan membungkus header JWT dan payload untuk di-encode menggunakan base64. Lalu itu di-hash dengan key yang ada di server sesuai tipe algoritma JWT yang dipakai dan itu dijadikan sebagai signature. Ini yang akan dikirim ke browser dan akan digunakan browser di tiap request untuk Authorization. Saat request diterima server maka akan diverifikasi dengan cara header & payload di-hash ulang dengan key yang digunakan kemudian dibandingkan dengan JWT yang dikirim browser. Jika valid berarti Authorization berhasil.

JWS + Secret Key

Kita bisa membuat signature menggunakan Secret Key yang sama. Jadi, saat Authentication & Authorization, key yang dipakai adalah key yang sama. Kombinasi ini adalah pilihan yang populer yang karena komputasinya paling cepat dan ga ribet. Kekurangannya, kalau Secret Key bocor maka hacker bisa bikin token sendiri tanpa perlu Authentication. Ini cocok untuk aplikasi yang tempat Authentication & Authorization di server yang sama atau pada server Monolith. Kalau Authentication & Authorization di tempat berbeda artinya SecretKey harus disebar di beberapa server sehingga peluang bocornya lebih tinggi.

Sekarang kita coba bikin codenya. Pertama kita perlu generate Secret Key terlebih dulu. Sebenarnya tulis String sendiri juga bisa sih, tapi sebaiknya ini di-generate biar lebih aman. Kita akan generate menggunakan algoritma HmacSHA256.

Generate SecretKey
Copy
public static void generateKey() throws NoSuchAlgorithmException {
	KeyGenerator keyGenerator = KeyGenerator.getInstance("HmacSHA256");
	keyGenerator.init(256);
	SecretKey secretKey = keyGenerator.generateKey();
	String secretKeyStr = Base64.getEncoder().encodeToString(secretKey.getEncoded());
	System.out.println("secretKeyStr = " + secretKeyStr);
}

Setelah di-generate Secret Key tersebut disimpan di tempat yang aman seperti In-Memory DB khusus security, Vault, environment variables, atau sejenisnya. Secret Key yang sama akan digunakan berkali-kali untuk Authentication & Authorization.

Contoh Penggunaan
Copy
public static String generateJwtToken(String username, SecretKey secretKey) {
	Map<String, String> claims = Maps.of("role", "superuser").and("realName", "ferry").build();
	return Jwts.builder()
			.subject(username)
			.claims(claims)
			.expiration(new Date(System.currentTimeMillis() * 30 * 60 * 1000))
			.signWith(secretKey)
			.compact();
}

public static Jws<Claims> parseJwtToken(String token, SecretKey secretKey) {
	if(token == null || token.isBlank()){
		return null;
	}
	try {
		return Jwts.parser()
				.verifyWith(secretKey)
				.build()
				.parseSignedClaims(token);
	} catch (Exception e) {
		return null;
	}
}

private static void jwsPublic(){
	String secretKeyStr = "pKIhWfkTQ4J/HOkZIJQpb7iojF8HPWJAUIkeCSZz/ko=";
	SecretKey secretKey = new SecretKeySpec(Base64.getDecoder().decode(secretKeyStr), "HmacSHA256");
	String jwtToken = generateJwtToken("ferry", secretKey);
	System.out.println("jwtToken = " + jwtToken);

	Jws<Claims> claims = parseJwtToken(jwtToken, secretKey);
	System.out.println("claims = " + claims);
}

Dari Secret Key tadi, kita akan bungkus kembali ke objek SecretKey, lalu generate tokennya. Ini yang akan disimpan oleh web nanti. Saat Authorization, web akan kembali mengirim token tersebut dan kita verifikasi lewat parseJwtToken() menggunakan SecretKey yang sama. Jika valid maka ga akan ada error dan kita akan mendapatkan isi payload untuk validasi data lebih lanjut.

JWS + Public/Private Key

Di sini saat Authentication kita akan sign menggunakan Private Key. Saat Authorization kita akan verifikasi menggunakan Public Key. Dengan begini peluang key bocor lebih kecil karena beda key. Kekurangannya, ini komputasinya sedikit lebih lambat dibanding menggunakan Secret Key. Ini cocok untuk aplikasi yang tempat Authentication & Authorization di tempat berbeda. Private Key hanya disimpan di satu server Authentication aja, sedangkan Public Key disimpan server lain yang bisa Authorization. Fungsi dari Public Key hanya untuk verifikasi. Meskipun itu kita sebar ke berbagai server maka ga akan ngaruh karena token hanya bisa di-sign menggunakan Private Key.

Untuk penggunaannya kita perlu generate pasangan Private Key dan Public Key dulu menggunakan algoritma RSA.

Generate KeyPair
Copy
private static void generateKeyPair() throws NoSuchAlgorithmException{
	KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
	kpg.initialize(2048);
	KeyPair keyPair = kpg.generateKeyPair();

  PublicKey publicKey = keyPair.getPublic();
	System.out.println("publicKey = " + Base64.getEncoder().encodeToString(publicKey.getEncoded()));

	PrivateKey privateKey = keyPair.getPrivate();
	System.out.println("privateKey = " + Base64.getEncoder().encodeToString(privateKey.getEncoded()));
}

Oh ya, Key ini harus berpasangan dan pasangannya jangan sampai hilang. Kalau salah satu hilang maka token jadi ga bisa digunakan. Private Key disimpan di server Authentincation, sedangkan Public Key disebar ke server yang melakukan Authorization. Public Key dan Private Key ini juga harus disimpan di tempat yang aman di masing-masing server.

Contoh Penggunaan
Copy
public static String generateJwtToken(String username, PrivateKey privateKey) {
	Map<String, String> claims = Maps.of("role", "superuser").and("realName", "ferry").build();
	return Jwts.builder()
			.subject(username)
			.claims(claims)
			.expiration(new Date(System.currentTimeMillis() * 30 * 60 * 1000))
			.signWith(privateKey)
			.compact();
}

public static Jws<Claims> parseJwtToken(String token, PublicKey publicKey) {
	if(token == null || token.isBlank()){
		return null;
	}
	try {
		return Jwts.parser()
				.verifyWith(publicKey)
				.build()
				.parseSignedClaims(token);
	} catch (Exception e) {
		return null;
	}
}

private static void jwsPrivate() throws NoSuchAlgorithmException, InvalidKeySpecException{
	KeyFactory keyFactory = KeyFactory.getInstance("RSA");
	String privateKeyStr = getPrivateKeyStr();
	PrivateKey privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKeyStr)));

	String jwtToken = generateJwtToken("ferry", privateKey);
	System.out.println("jwtToken = " + jwtToken);

	String publicKeyStr = getPublicKeyStr();
	PublicKey publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(Base64.getDecoder().decode(publicKeyStr)));

	Jws<Claims> claims = parseJwtToken(jwtToken, publicKey);
	System.out.println("claims = " + claims);
}

Dari Private Key tadi kita akan bungkus kembali ke objek PrivateKey, lalu generate tokennya. Ini yang akan disimpan oleh web nanti. Saat Authorization, web akan kembali mengirim token tersebut. Di server Authorization, Public Key tadi akan dibungkus kembali ke objek PublicKey lalu kita pakai untuk verifikasi parseJwtToken(). Jika valid maka ga akan ada error dan kita akan mendapatkan isi payload untuk validasi data lebih lanjut.

JWE

JWE adalah alternatif dari JWS. Di JWS, payload hanya di-encode dengan Base64 sehingga bisa dibaca. Ini ga aman kalau ada data sensitif di dalamnya. Solusinya dengan JWE adalah payload dienkripsi. JWE terdiri dari 5 bagian yang juga dipisahkan simbol titik dan semuanya di-encode dengan Base64:

  • Header: berisi info algoritma yang dipakai, tipe enkripsi yang digunakan, tipe JWT, dan hal-hal teknis lainnya;
  • CEK (Content Encryption Key): ini adalah String yang di-generate dan dienkripsi untuk mengunci payload, ini fungsinya seperti password;
  • Initialization Vector (Random Bytes): ini berguna sebagai salt agar hasil enkripsinya ga bisa ditebak meskipun menggunakan data yang sama;
  • Ciphertext: ini adalah data payload yang dienkripsi menggunakan algoritma yang dipilih dan dikunci menggunakan CEK dan Initialization Vector yang ada di token;
  • Authentication Tag: ini semacam checksum dari Header, CEK, Initialization Vector, dan Ciphertext untuk menjamin integritas token ga bisa diubah dari luar;
Copy
${Base64(header)}.${Base64(encrypt(CEK + key))}.${Base64(random)}.${Base64(encrypt(payload + CEK + random + key))}.${Base64(checksum(ciphertext + random + CEK + header))}

Setelah berhasil Authentication, nanti server akan membungkus header JWT di-encode menggunakan base64. Lalu generate CEK, ini adalah password enkripsi. CEK juga akan dienkripsi pada token menggunakan Secret Key atau Public/Private Key. Selanjutnya generate random bytes sebagai Initialization Vector. Kemudian payload dienkripsi sebagai Ciphertext sesuai algoritma enkripsi yang dipakai menggunakan Secret Key atau Public/Private Key dan dikunci dengan CEK + Initialization Vector. Terakhir generate checksum dari Header, CEK, Initialization Vector, dan Ciphertext sebagai Authentication Tag untuk menjamin data yang didekripsi ga bisa diubah dari luar. Ini yang akan dikirim ke browser dan akan digunakan browser di tiap request untuk Authorization. Saat request diterima server maka akan diverifikasi dengan cara mengekstrak CEK yang terenkripsi dari token lalu CEK didekripsi menggunakan Secret Key atau Public/Private Key. Kemudian digenerate ulang checksum dari Header, CEK yang sudah didekripsi, Initialization Vector, dan Ciphertext, jika sama dengan Authentication Tag pada token artinya token tersebut valid. Selanjutnya dekripsi Ciphertext menjadi payload kembali menggunakan Secret Key atau Public/Private Key dan buka kuncinya menggunakan CEK + Initialization Vector. Jika terbuka berarti Authorization berhasil.

JWE + Secret Key

Kelebihan dan kekurangannya mirip dengan JWS + Secret Key. Bedanya di sini Secret Key harus disimpan dengan algoritma AES. Ini lebih aman karena data di tokennya ga bisa dibaca. Kekurangannya ini lebih lambat daripada JWS + Secret Key karena algoritmanya kompleks. Ini cocoknya untuk token yang menyimpan payload sensitif.

Contoh Penggunaan
Copy
public static String generateJweToken(String username, SecretKey secretKey) {
	Map<String, String> claims = Maps.of("role", "superuser").and("realName", "ferry").build();
	return Jwts.builder()
			.subject(username)
			.claims(claims)
			.expiration(new Date(System.currentTimeMillis() * 30 * 60 * 1000))
			.encryptWith(secretKey, Jwts.KEY.A256KW, Jwts.ENC.A256GCM)
			.compact();
}

public static Jwe<Claims> parseJweToken(String token, SecretKey secretKey) {
	if(token == null || token.isBlank()){
		return null;
	}
	try {
		return Jwts.parser()
				.decryptWith(secretKey)
				.build()
				.parseEncryptedClaims(token);
	} catch (Exception e) {
		return null;
	}
}

private static void jwePublic(){
	String secretKeyStr = "pKIhWfkTQ4J/HOkZIJQpb7iojF8HPWJAUIkeCSZz/ko=";
	SecretKey secretKey =  new SecretKeySpec(Base64.getDecoder().decode(secretKeyStr), "AES");

	String jwtToken = generateJweToken("ferry", secretKey);
	System.out.println("jwtToken = " + jwtToken);

	Jwe<Claims> claims = parseJweToken(jwtToken, secretKey);
	System.out.println("claims = " + claims);
}

Cara penggunaannya sama kayak JWS + Secret Key. Bedanya hanya di alogritmanya saja. Untuk algoritma enkripsi kita menggunakan A256GCM. Untuk algoritma Secret Key menggunakan A256KW.

JWE + Public/Private Key

Ini kombinasi yang paling aman karena datanya dienkripsi dan key untuk Authentication dan Authorization berbeda. Kekurangannya, ini yang paling lambat karena algoritmanya paling kompleks. Ini cocok untuk aplikasi yang terintegrasi dengan sistem eksternal dan datanya sensitif sehingga risikonya harus sangat minimal.

Contoh Penggunaan
Copy
public static String generateJweToken(String username, PublicKey publicKey) {
	Map<String, String> claims = Maps.of("role", "superuser").and("realName", "ferry").build();
	return Jwts.builder()
			.subject(username)
			.claims(claims)
			.expiration(new Date(System.currentTimeMillis() * 30 * 60 * 1000))
			.encryptWith(publicKey, Jwts.KEY.RSA_OAEP_256, Jwts.ENC.A256GCM)
			.compact();
}

public static Jwe<Claims> parseJweToken(String token, PrivateKey privateKey) {
	if(token == null || token.isBlank()){
		return null;
	}
	try {
		return Jwts.parser()
				.decryptWith(privateKey)
				.build()
				.parseEncryptedClaims(token);
	} catch (Exception e) {
		return null;
	}
}

private static void jwePrivate() throws NoSuchAlgorithmException, InvalidKeySpecException{
	KeyFactory keyFactory = KeyFactory.getInstance("RSA");
	String publicKeyStr = getPublicKeyStr();
	PublicKey publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(Base64.getDecoder().decode(publicKeyStr)));

	String privateKeyStr = getPrivateKeyStr();
	PrivateKey privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKeyStr)));

	String jwtToken = generateJweToken("ferry", publicKey);
	System.out.println("jwtToken = " + jwtToken);

	Jwe<Claims> claims = parseJweToken(jwtToken, privateKey);
	System.out.println("claims = " + claims);
}

Berbeda dengan JWS + Public/Private Key, di sini kita membuat token menggunakan Public Key. Sedangkan Private Key digunakan untuk verifikasi. Jadi, token tersebut hanya bisa diverifikasi oleh server yang dituju doang. Kita menggunakan algoritma key RSA_OAEP_256 dan algoritma enkripsi A256GCM.

Verdict

JWT ini algoritmanya fleksibel dan bisa dipilih sesuka hati. Saking fleksibelnya, kita harus hati-hati dalam menentukan algoritma yang dipilih. Ini salah satu kritikan orang-orang terkait JWT karena bisa saja algoritma yang diimplementasi lemah. JWT juga menawarkan beberapa pilihan seperti JWS + Secret Key, JWS + Public/Private Key, JWE + Secret Key, dan JWE + Public/Private Key. JWS + Secret Key adalah pilihan yang paling populer karena paling simple dan performanya paling cepat. Semakin strict algoritma semakin aman tapi secara performa akan makin lambat. JWT ini juga dikritik karena data payload bisa dibaca pada JWS + Secret Key sehingga muncul kompetitornya yang lebih disiplin seperti PASETO. Selama implementasi JWT yang kita lakukan sudah sesuai best practice menurut gw JWT masih worth it.

© 2026 · Ferry Suhandri