Skip to content
ecdsa.com
Ruby

ruby-jwt (jwt gem)

ruby-jwt, the jwt gem used by most Ruby and Rails applications: an explicit algorithm is required, key types are checked, ES256K is included — but EdDSA moved to a separate gem in 3.x and short HMAC secrets are accepted silently.
License
MIT
Checked version
3.3.0 ·
Source
github.com/jwt/ruby-jwt
Documentation
github.com/jwt/ruby-jwt#readme

Compatibility at a glance

JOSE and JWT

JWS algorithms
HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384ES512ES256KEdDSA

JWT.encode and JWT.decode cover HS, RS, PS and ES at 256/384/512 and ES256K on top of Ruby's openssl gem. EdDSA is no longer part of the jwt gem: with only jwt 3.3.0 loaded, encoding EdDSA failed with Unsupported signing method, and it worked after adding jwt-eddsa — whose native ed25519 dependency needs a C compiler at install time, which slim container images lack.

Verification requires naming the algorithm: JWT.decode(token, key, true) without algorithm: failed with Expected a different algorithm. Unsigned tokens are refused when verifying, and readable only with verification switched off (JWT.decode(token, nil, false)), which code should never do for tokens it trusts. Key types are checked: an HS256 token forged with the EC key's PEM text failed with HMAC key expected to be a String when the verification key was the EC key object — but passing the PEM string as the HS256 key verified it, so keep keys as OpenSSL::PKey objects. ES256 refuses a P-384 key at signing time.

Claims checks are opt-in: no leeway, and exp is optional unless listed in required_claims, as the sample does together with the issuer. HMAC secrets of any length are accepted — a 5-byte key signed and verified without a warning — so check secrets with the JWT secret checker.

ES256 signatures are 64 bytes of raw r‖s; a DER signature fails with Signature verification failed. Paste a token into the JWT debugger to see which encoding it carries.

Facts and the checks behind them

Nothing below is copied from documentation. The check content/libraries/verify/ruby-jwt/run.sh ran ruby-jwt (jwt gem) 3.3.0 in the container image ruby:4.0.7-slim-trixie on October 20, 2026; the page must match what it printed, and each fact lists those lines.

  • JWS algorithms

    HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384ES512ES256KEdDSA

    HS, RS, PS and ES at 256/384/512 and ES256K signed and verified with the jwt gem; EdDSA only after adding the jwt-eddsa gem.

    Recorded by the check
    fact jws-algorithms = HS256, HS384, HS512, RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512, ES256K, EdDSA
    evidence jws-algorithms: EdDSA with only the jwt gem loaded: JWT::EncodeError: Unsupported signing method; after require "jwt/eddsa": ok
  • ES256 signature encoding

    raw

    ES256 signatures are 64 bytes of raw r‖s.

    Recorded by the check
    fact jws-ecdsa-encoding = raw
  • DER signature inside a JWS

    rejected

    A DER-encoded ES256 signature fails with JWT::VerificationError: Signature verification failed.

    Recorded by the check
    fact jws-der-signature = rejected
    evidence jws-der-signature: JWT::VerificationError: Signature verification failed
  • Algorithm allow-list

    required

    Verifying without algorithm: fails with JWT::IncorrectAlgorithm: Expected a different algorithm.

    Recorded by the check
    fact jwt-algorithm-allowlist = required
    evidence jwt-algorithm-allowlist: JWT.decode with verify true and no algorithm option: JWT::IncorrectAlgorithm: Expected a different algorithm
  • Unsigned tokens (alg none)

    rejected

    Unsigned tokens fail verification with Expected a different algorithm; only verify = false reads them.

    Recorded by the check
    fact jwt-alg-none = rejected
    evidence jwt-alg-none: verify true, algorithm ES256: JWT::IncorrectAlgorithm: Expected a different algorithm; verify false: ok
  • Public key used as an HMAC secret

    rejected

    An HS256 token keyed with the PEM text failed against the EC key object with HMAC key expected to be a String, but verified when the PEM string itself was passed as the HS256 key.

    Recorded by the check
    fact jwt-hmac-public-key = rejected
    evidence jwt-hmac-public-key: algorithms [ES256, HS256] with the EC key object: JWT::VerificationKeyError: HMAC key expected to be a String
    evidence jwt-hmac-public-key: algorithm HS256 with the PEM string as key: ok
  • Curve checked against alg

    enforced

    Signing ES256 with a P-384 key fails with payload algorithm is ES256 but ES384 signing key was provided.

    Recorded by the check
    fact jwt-ecdsa-curve-check = enforced
    evidence jwt-ecdsa-curve-check: JWT::EncodeError: payload algorithm is ES256 but ES384 signing key was provided
  • Default clock skew

    0s

    No leeway by default: a token expired three seconds earlier was rejected.

    Recorded by the check
    fact jwt-clock-skew = 0s
  • Expiry claim required

    no

    A token without exp decoded; required_claims: ["exp"] makes it fail with Missing required claim exp.

    Recorded by the check
    fact jwt-exp-required = no
    evidence jwt-exp-required: with required_claims: ["exp"]: JWT::MissingRequiredClaim: Missing required claim exp
  • Short HMAC secret

    accepted

    A 5-byte HS256 secret signed and verified without any warning.

    Recorded by the check
    fact jwt-hmac-short-secret = accepted
    evidence jwt-hmac-short-secret: 5-byte HS256 key: ok

Usage

Encode an ES256 token and decode it with the algorithm named, exp and iss required and 30 seconds of leeway.
require "jwt"

key = OpenSSL::PKey::EC.generate("prime256v1")
public_key = OpenSSL::PKey.read(key.public_to_pem)
payload = { "sub" => "user-42", "iss" => "https://auth.example.com", "exp" => Time.now.to_i + 600 }
token = JWT.encode(payload, key, "ES256")

# Name the algorithm, require exp and iss, and allow some clock drift.
claims, header = JWT.decode(token, public_key, true,
                            algorithm: "ES256",
                            required_claims: %w[exp iss],
                            iss: "https://auth.example.com", verify_iss: true,
                            leeway: 30)
puts "sub: #{claims['sub']}, alg: #{header['alg']}"

The snippet harness runs every sample verbatim in the container image ruby:4.0.7-slim-trixie, and the run fails unless the output matches.

Setup
gem install --silent --no-document jwt -v 3.3.0
Command
ruby usage.rb
Output must match (regular expression)
sub: user-42, alg: ES256

Libraries