Decode a JWT in Java with JJWT

Decode JWT tokens in Java using JJWT (io.jsonwebtoken). Read payload without verification and perform full verified decode. Maven and Gradle examples.

ZERO UPLOAD · ALL LOCAL
  1. Copy your JWT token — it looks like three Base64 sections separated by dots (eyJ…)
  2. Paste the token into the input field — decoding happens automatically on paste.
  3. Read the Header panel: it shows the signing algorithm (alg) and token type (typ).
  4. Read the Payload panel: it shows all claims — user ID, roles, issued-at (iat), and expiry (exp).
  5. Check the expiry status indicator — it shows whether the token is currently valid or expired, with a human-readable time delta.
  6. Note: the tool decodes only — it does not verify the signature. Never trust decoded claims in a security context without server-side verification.

What this page covers

  • Jwts.parser().parseClaimsJwt() reads claims without verifying, using JJWT's builder API
  • claims.get(name, Type.class) reads a custom claim with an explicit type instead of a raw Map cast

HEADER
          
PAYLOAD
          
EXPIRY

Decode a JWT in Java with JJWT

If a Java route returns a token parsing error, JJWT is often the library that pinpoints the failure. JJWT is the most widely used JWT library for Java and Android, published under io.jsonwebtoken on Maven Central. It provides a fluent builder API for both token creation and parsing, covering decode-without-verification and full cryptographic verification paths. Adding it to a Gradle project requires two lines: jjwt-api and jjwt-impl.1 Paste any token into the decoder above for instant offline inspection without writing Java application code, or follow the patterns below for application integration.

The safer debugging approach uses browser-based inspection rather than pasting tokens into a server-backed tool. Because every character of a JWT is transmitted in server-backed decoders, production tokens become a liability. Offline inspection in your browser reveals all claims: sub, exp, realm_access, or any custom field: with no token transmission risk. For Java middleware that must read claims programmatically, JJWT's parser handles both the unsigned inspection case and the fully verified case with equal conciseness.

Library and setup

Add JJWT to your Maven pom.xml with two dependencies: io.jsonwebtoken:jjwt-api (the interface layer) and io.jsonwebtoken:jjwt-impl (the runtime implementation)2; for RS256 or ES256 verification, also add io.jsonwebtoken:jjwt-jackson for JSON parsing if your project does not already include Jackson. In Gradle, add the same two artifacts to the dependencies block, where the api artifact provides the public interfaces while impl provides the runtime implementations that handle the actual cryptographic operations. Importing io.jsonwebtoken.Jwts is the only import needed for most parsing operations, and the fluent builder pattern that JJWT uses makes it straightforward to chain configuration calls for signing keys, required claims, and expiration checks.

Keeping unsigned parsing out of production

The unsigned parser is useful when you inspect test fixtures or reproduce a customer report because it reads the payload without requiring a signing key. It should not live in route guards, because it accepts tokens without issuer trust, which means an unsigned parser in a production route guard effectively disables authentication entirely and allows any client to construct a JWT with arbitrary claims that the server will accept as long as the Base64Url encoding is structurally valid.

Decoding without verification

Parsing a JWT payload without signature verification requires removing the signature section first, because the unsigned parser rejects any token that still carries a signature. Strip everything after the second dot: String unsignedToken = token.substring(0, token.lastIndexOf('.') + 1). Then call Jwts.parser().build().parseClaimsJwt(unsignedToken) to get the Claims object3, which works for any token regardless of the algorithm specified in the header.

Reading claims from the result uses typed accessor methods: getSubject() for sub, getExpiration() for exp, or get('custom', String.class) for provider-specific claims like realm_access or cognito:groups. The Claims interface extends java.util.Map, so you can also iterate over all entries when you need to inspect every claim in a token from an unfamiliar provider.

Reading specific claims

The Claims object returned by parseClaimsJwt exposes typed accessors for all registered JWT claims.3 getSubject() returns the sub claim as a String; getExpiration() returns a java.util.Date. Building on this, getIssuedAt() returns the iat timestamp, getIssuer() returns iss, and getAudience() returns the aud claim as a Set<String>, which means checking audience validity is a simple contains() call rather than a string comparison.

For custom or provider-specific claims, like realm_access in Keycloak tokens, use get('realm_access', Map.class) with the expected Java type to avoid ClassCastException at runtime. Always print the full Claims object before selecting individual paths from an unfamiliar provider, because nested objects require exact type declarations that are difficult to guess without seeing the actual JSON structure first.

Full signature verification with JJWT using RS256 keys

JJWT handles RS256 verification by accepting a PublicKey or JWK object as the signing key in the parser. Fetch the public key from your provider's JWKS endpoint, deserialize it using JJWT's JWKSet parser, then call Jwts.parser().verifyWith(publicKey).requireIssuer(expectedIss).requireAudience(expectedAud).build().parseSignedClaims(token) to perform full verified parsing that checks the cryptographic signature, expiry, issuer, and audience in a single call. The method throws JwtException on any failure, giving your error handler enough context to return the correct HTTP status code for each specific validation failure.

Dynamic key selection with Locator

For dynamic JWKS key selection by kid, use Jwts.parser().keyLocator(locator) where locator implements Locator&lt;Key&gt;. Override locate(JwsHeader header) to read header.getKeyId() and return the matching public key from your JWKS cache. JJWT calls this method once per verification, giving your locator full access to the token header for key selection before the signature check runs. Caching the JWKS response inside the locator and only refreshing when an unknown kid appears keeps verification fast while still handling key rotation without manual intervention or application restarts.

Placing the cache inside the locator keeps key selection fast even during an active rotation event when two keys are valid at once. The verifier asks the locator for the correct key on every call, so new tokens verify against the fresh key while old tokens still match the previous one. This design removes manual rotation handling from your route code entirely.

Keeping unsigned parsing out of route guards

Keep unsigned parsing out of route guards because it accepts tokens without issuer trust and effectively disables authentication for any client that can construct a valid Base64Url-encoded payload. In production, pass the original signed token to parseSignedClaims and let JJWT reject the request whenever the signature, expiry, issuer, or audience fails validation. A common code-review check is to search the codebase for all calls to parseClaimsJwt and verify that none of them sit inside a filter, interceptor, or any other component that runs before the request reaches your business logic.4 For a quick look at a token's shape before you write any Java, reading JWT claims without a JVM renders the same header and claims that parseClaimsJwt would return.

When to use this

Use JJWT when your Java or Android backend reads JWTs from any standard provider. The unsigned parse path suits test fixtures and debugging; the signed parse path suits production API middleware.

Examples

Decode JWT payload without verification in Java

Before
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;

String token = "eyJ...";
// Strip signature for unsigned parsing
String unsigned = token.substring(0, token.lastIndexOf('.') + 1);
Claims claims = Jwts.parser()
    .build()
    .parseClaimsJwt(unsigned)
    .getBody();

System.out.println(claims.getSubject());
System.out.println(claims.getExpiration());

Remove the signature section (third JWT part) before calling parseClaimsJwt: the unsigned parser rejects signed tokens.

Read a custom claim from a parsed JWT

Before
Claims claims = Jwts.parser()
    .build()
    .parseClaimsJwt(unsigned)
    .getBody();

// Standard claim
String sub = claims.getSubject();

// Custom claim with type
String role = claims.get("role", String.class);
Map<?, ?> realmAccess = claims.get("realm_access", Map.class);

Always specify the expected Java type in get() to avoid raw Map casting.

Sources
  1. 1.

    jwtk/jjwt, "Java JWT: JSON Web Token for Java and Android," github.com, accessed June 2026. https://github.com/jwtk/jjwt

  2. 2.

    Maven Central, "io.jsonwebtoken:jjwt-api," central.sonatype.com, accessed June 2026. https://central.sonatype.com/artifact/io.jsonwebtoken/jjwt-api

  3. 3.

    JJWT, "Claims," javadoc.io, accessed June 2026. https://javadoc.io/static/io.jsonwebtoken/jjwt-api/0.13.0/io/jsonwebtoken/Claims.html

  4. 4.

    JJWT, "Jwts," javadoc.io, accessed June 2026. https://javadoc.io/static/io.jsonwebtoken/jjwt-api/0.13.0/io/jsonwebtoken/Jwts.html

FAQ