Skip to content
ecdsa.com
Java · Kotlin

JJWT (io.jsonwebtoken)

Les Hazlewood's JJWT, a widely used JWT library for Java and Android: the verification key fixes the acceptable algorithm, unsigned tokens need an explicit unsecured() parser, and weak HMAC keys are refused outright.
License
Apache-2.0
Checked version
0.13.0 ·
Source
github.com/jwtk/jjwt
Documentation
github.com/jwtk/jjwt#readme

Compatibility at a glance

JOSE and JWT

JWS algorithms
HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384ES512EdDSA

JJWT 0.12 and later expose a fluent builder and parser — Jwts.builder()…signWith(key, alg) and Jwts.parser().verifyWith(key)…parseSignedClaims(token) — with Jwts.SIG holding the algorithms. It ships as three artifacts: jjwt-api at compile time, jjwt-impl and a JSON binding such as jjwt-jackson at run time. 0.13.0 round-tripped HS, RS, PS and ES at 256/384/512 and EdDSA; ES256K and the Ed25519 and Ed448 names are not in Jwts.SIG.

The parser derives the allowed algorithm from the verification key, and its errors say so plainly. An RS256 token against an EC public key, and an HS256 token keyed with the EC key's PEM text, both failed with UnsupportedJwtException explaining that the key may not be used for that algorithm. The protection is the key type, not a list: a parser given Keys.hmacShaKeyFor(pemBytes) verified that forged token, so never build HMAC keys from material that could be public.

Unsigned tokens are refused by parseSignedClaims with a message citing RFC 7518 §3.6, and readable only through Jwts.parser().unsecured()…parseUnsecuredClaims. Weak HMAC keys never get that far: Keys.hmacShaKeyFor rejected a 16-byte key with WeakKeyException, as RFC 7518 requires keys at least as long as the hash. ES256 refuses a P-384 key at signing time.

Two defaults need attention. There is no clock skew allowance — a token expired three seconds earlier failed — and exp is optional, so set clockSkewSeconds and check expiry and issuer as the sample does. ES256 signatures are the 64-byte raw r‖s; a DER signature is reported precisely (Provided signature is 576 bits (72 bytes) but ES256 signatures must be exactly 512 bits (64 bytes)), one of the few libraries that names the encoding problem. Inspect tokens in the JWT debugger and HMAC keys with the JWT secret checker.

For JWE and JWK set handling at scale, compare with Nimbus JOSE + JWT.

Facts and the checks behind them

Nothing below is copied from documentation. The check content/libraries/verify/jjwt/run.sh ran JJWT (io.jsonwebtoken) 0.13.0 in the container image maven:3.9.16-eclipse-temurin-25 on October 20, 2026; the page must match what it printed, and each fact lists those lines.

  • JWS algorithms

    HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384ES512EdDSA

    HS, RS, PS and ES at 256/384/512 and EdDSA signed and verified; ES256K, Ed25519 and Ed448 are not in Jwts.SIG.

    Recorded by the check
    fact jws-algorithms = HS256, HS384, HS512, RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512, EdDSA
    evidence jws-algorithms: ES256K: not in Jwts.SIG
    evidence jws-algorithms: Ed25519: not in Jwts.SIG
    evidence jws-algorithms: Ed448: not in Jwts.SIG
  • 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 Provided signature is 576 bits (72 bytes) but ES256 signatures must be exactly 512 bits (64 bytes).

    Recorded by the check
    fact jws-der-signature = rejected
    evidence jws-der-signature: SignatureException: Unable to verify Elliptic Curve signature using provided ECPublicKey: Provided signature is 576 bits (72 bytes) but ES256 signatures must be exactly 512 bits (64 bytes) per [RFC 7518, Section 3.4 (validation)](https://www.rfc-editor.org/rfc/rfc7518.html#section-3.4).
  • Algorithm allow-list

    from-key

    The verification key determines the algorithm: an RS256 token against an EC public key failed with UnsupportedJwtException.

    Recorded by the check
    fact jwt-algorithm-allowlist = from-key
    evidence jwt-algorithm-allowlist: RS256 token, verifyWith(EC public key): UnsupportedJwtException: The parsed JWT indicates it was signed with the 'RS256' signature algorithm, but the provided sun.security.ec.ECPublicKeyImpl key may not be used to verify RS256 signatures.  Because the specified key reflects a specific and expected algorithm, and the JWT does not reflect this algorithm, it is likely that the JWT was not expected and therefore should not be trusted.  Another possibility is that the parser was provided the incorrect signature verification key, but this cannot be assumed for security reasons.
  • Unsigned tokens (alg none)

    opt-in

    parseSignedClaims rejects unsigned tokens; Jwts.parser().unsecured() with parseUnsecuredClaims reads them.

    Recorded by the check
    fact jwt-alg-none = opt-in
    evidence jwt-alg-none: parseSignedClaims with the EC key: UnsupportedJwtException: Unsecured JWSs (those with an 'alg' (Algorithm) header value of 'none') are disallowed by default as mandated by https://www.rfc-editor.org/rfc/rfc7518.html#section-3.6. If you wish to allow them to be parsed, call the JwtParserBuilder.unsecured() method, but please read the security considerations covered in that method's JavaDoc before doing so. Header: {alg=none, typ=JWT}
    evidence jwt-alg-none: parser().unsecured().parseUnsecuredClaims: verified
  • Public key used as an HMAC secret

    rejected

    An HS256 token keyed with the PEM text was rejected against the EC public key, but verified with Keys.hmacShaKeyFor(pemBytes).

    Recorded by the check
    fact jwt-hmac-public-key = rejected
    evidence jwt-hmac-public-key: verifyWith(EC public key): UnsupportedJwtException: The parsed JWT indicates it was signed with the 'HS256' signature algorithm, but the provided sun.security.ec.ECPublicKeyImpl key may not be used to verify HS256 signatures.  Because the specified key reflects a specific and expected algorithm, and the JWT does not reflect this algorithm, it is likely that the JWT was not expected and therefore should not be trusted.  Another possibility is that the parser was provided the incorrect signature verification key, but this cannot be assumed for security reasons.
    evidence jwt-hmac-public-key: verifyWith(Keys.hmacShaKeyFor(PEM bytes)): verified
  • Curve checked against alg

    enforced

    Signing ES256 with a P-384 key fails: the 'ES256' algorithm requires EC Keys with 256 bits.

    Recorded by the check
    fact jwt-ecdsa-curve-check = enforced
    evidence jwt-ecdsa-curve-check: 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) per [RFC 7518, Section 3.4](https://www.rfc-editor.org/rfc/rfc7518.html#section-3.4).
  • Default clock skew

    0s

    No clock skew 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 parsed successfully.

    Recorded by the check
    fact jwt-exp-required = no
  • Short HMAC secret

    rejected

    Keys.hmacShaKeyFor rejected a 16-byte key with WeakKeyException.

    Recorded by the check
    fact jwt-hmac-short-secret = rejected
    evidence jwt-hmac-short-secret: Keys.hmacShaKeyFor(16 bytes): WeakKeyException: The specified key byte array is 128 bits which is not secure enough for any JWT HMAC-SHA algorithm.  The JWT JWA Specification (RFC 7518, Section 3.2) states that keys used with HMAC-SHA algorithms MUST have a size >= 256 bits (the key size must be greater than or equal to the hash output size).  Consider using the Jwts.SIG.HS256.key() builder (or HS384.key() or HS512.key()) to create a key guaranteed to be secure enough for your preferred HMAC-SHA algorithm.  See https://tools.ietf.org/html/rfc7518#section-3.2 for more information.

Usage

Issue an ES256 token and parse it with the key, the issuer required and 30 seconds of clock skew.
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import java.security.KeyPair;
import java.util.Date;

public class Usage {
    public static void main(String[] args) {
        KeyPair pair = Jwts.SIG.ES256.keyPair().build();
        String token = Jwts.builder()
            .subject("user-42")
            .issuer("https://auth.example.com")
            .expiration(new Date(System.currentTimeMillis() + 600_000))
            .signWith(pair.getPrivate(), Jwts.SIG.ES256)
            .compact();

        // The key decides which algorithms are acceptable; require iss and exp explicitly
        // and allow some clock skew (the default is none).
        Claims claims = Jwts.parser()
            .verifyWith(pair.getPublic())
            .requireIssuer("https://auth.example.com")
            .clockSkewSeconds(30)
            .build()
            .parseSignedClaims(token)
            .getPayload();
        System.out.println("sub: " + claims.getSubject() + ", has exp: " + (claims.getExpiration() != null));
    }
}

The snippet harness runs every sample verbatim in the container image maven:3.9.16-eclipse-temurin-25, and the run fails unless the output matches.

Setup
mvn -q -B dependency:copy-dependencies -DoutputDirectory=lib >/dev/null
Command
java -cp 'lib/*' Usage.java
Output must match (regular expression)
sub: user-42, has exp: true

Libraries