Skip to content
ecdsa.com

Sign an ES256 JWT · Ruby

How to sign a JWT with ES256 in Ruby

To sign a JWT with ES256 in Ruby, the jwt gem's JWT.encode takes the payload hash, an OpenSSL::PKey::EC private key, "ES256" and the header fields. The gem signs with the openssl library and writes the 64-byte JWS signature; the run checks the token with OpenSSL against the public key.

Tested with jwt (ruby-jwt) 3.3.0 (Ruby 4.0.7, openssl gem 4.0.2).

sign.rb — ES256 with the jwt gem
require "jwt" # gem install jwt -v 3.3.0
require "openssl"
private_key = OpenSSL::PKey.read(File.read("key.pem")) # an OpenSSL::PKey::EC, not the PEM string
now = Time.now.to_i
payload = {
iss: "https://issuer.example.com",
sub: "user-42",
aud: "api://orders",
iat: now,
exp: now + 15 * 60
}
puts JWT.encode(payload, private_key, "ES256", { kid: "2026-10", typ: "JWT" })

How it works

  1. OpenSSL::PKey.read loads the PEM (PKCS#8 or EC PRIVATE KEY) into an OpenSSL::PKey::EC.
  2. JWT.encode(payload, key, "ES256", header_fields) signs; the fourth argument adds kid and typ to the header.
  3. Time.now.to_i gives whole seconds for iat and exp.
  4. The return value is the compact token string.

Gotchas

  • The key must be a key object: passing the PEM string raises JWT::EncodeError: The given key is a String. It has to be an OpenSSL::PKey::EC instance.
  • The gem does not add typ unless asked — the header is {"kid":…,"alg":"ES256"} without it. Pass typ: "JWT" if consumers check it.
  • The curve is checked: a P-384 key raises JWT::EncodeError: payload algorithm is ES256 but ES384 signing key was provided.
  • Use symbol or string keys consistently in the payload: { exp: 1, "exp" => 2 } is two hash keys, and the token carries {"exp":1,"exp":2} — JSON with a duplicate member, which verifiers resolve differently.

Related recipes