Skip to content
ecdsa.com

Sign an ES256 JWT · Kotlin

How to sign a JWT with ES256 in Kotlin

To sign a JWT with ES256 in Kotlin, JJWT's Jwts.builder() sets the header and claims and signs with a JCA private key and Jwts.SIG.ES256. JJWT is split into an API jar for compiling and implementation jars for runtime, which the Gradle declaration shows; the run checks the token with OpenSSL.

Tested with io.jsonwebtoken:jjwt 0.13.0 (Kotlin 2.4.21, jjwt-gson with Gson 2.14.0) and OpenJDK 25.0.4.1 (Eclipse Temurin 25.0.4.1+1).

build.gradle.kts — JJWT's three artifacts
// build.gradle.kts
dependencies {
implementation("io.jsonwebtoken:jjwt-api:0.13.0")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.13.0")
runtimeOnly("io.jsonwebtoken:jjwt-gson:0.13.0") // or jjwt-jackson
}
Sign.kt — ES256 with JJWT 0.13
import io.jsonwebtoken.Jwts
import java.io.File
import java.security.KeyFactory
import java.security.spec.PKCS8EncodedKeySpec
import java.time.Instant
import java.util.Base64
import java.util.Date
fun main() {
val pem = File("key.pem").readText()
val pkcs8 = Base64.getMimeDecoder().decode(pem.replace(Regex("-----[^-]+-----"), ""))
val privateKey = KeyFactory.getInstance("EC").generatePrivate(PKCS8EncodedKeySpec(pkcs8))
val now = Instant.now()
val token = Jwts.builder()
.header().keyId("2026-10").type("JWT").and()
.issuer("https://issuer.example.com")
.subject("user-42")
.audience().add("api://orders").and()
.issuedAt(Date.from(now))
.expiration(Date.from(now.plusSeconds(15 * 60)))
.signWith(privateKey, Jwts.SIG.ES256) // checks that the key is P-256
.compact()
println(token)
}

How it works

  1. The PKCS#8 PEM body is loaded with KeyFactory.getInstance("EC"); JJWT signs with any JCA PrivateKey.
  2. .header().keyId(…).type("JWT").and() writes kid and typ; issuer, subject, audience().add(…), issuedAt and expiration write the claims.
  3. signWith(privateKey, Jwts.SIG.ES256) checks that the key is a 256-bit EC key and produces the 64-byte JWS signature.
  4. Code compiles against jjwt-api alone; jjwt-impl and a JSON module (jjwt-gson or jjwt-jackson) are needed only at runtime.

Gotchas

  • Forgetting jjwt-impl compiles fine and fails at the first Jwts.builder(): UnknownClassException: Unable to load class named [io.jsonwebtoken.impl.DefaultJwtBuilder$Supplier] … Have you remembered to include the jjwt-impl.jar in your runtime classpath?.
  • audience().add(…) writes aud as an array even for one value. audience().single(…) writes a string but is deprecated; prefer the array unless a consumer cannot read it.
  • A P-384 key with Jwts.SIG.ES256 fails with SignatureException: Unable to compute ES256 signature. Cause: The provided Elliptic Curve signing key size (aka order bit length) is 384 bits (48 bytes), but the 'ES256' algorithm requires EC Keys with 256 bits (32 bytes)….
  • JJWT 0.12 renamed the builder methods (setSubject → subject, setExpiration → expiration) and replaced the SignatureAlgorithm enum with Jwts.SIG. Code written for 0.11 still compiles against 0.13, with a deprecation warning on every one of those calls — update it before they are removed.

Related recipes