By Docify

Compact JWS vs JWE: Decode Is Not Verify

• 6 min read

A JWT (RFC 7519, May 2015) is a claims set used as a JWS payload or as JWE plaintext. Compact JWS (RFC 7515) is three base64url segments: header.payload.signature. Compact JWE (RFC 7516 §9) is five. Header and payload are RFC 4648 §5 then JSON.parse— not ciphertext. Decode is not HMAC, RSA, ECDSA, or JWKS. Unsecured JWS uses alg none and a trailing dot. Nothing is uploaded.

“JWT” does not tell you whether the claims are readable. RFC 7519 puts the same JSON object in a JWS (signed or MACed) or a JWE (encrypted). The live JWT decoder counts compact segments first, then base64url-decodes only a three-part JWS. This page names that fork.

Three periods-of-two vs four

Compact JWS: header.payload.signature

RFC 7515 Compact Serialization is BASE64URL(UTF8(JWS Protected Header)) || '.' || BASE64URL(JWS Payload) || '.' || BASE64URL(JWS Signature) — exactly two period characters. The alphabet is RFC 4648 §5 (A-Z a-z 0-9 - _). Optional = padding is accepted here. Load sample is a compact HS256-shaped JWT whose header is {"alg":"HS256","typ":"JWT"} and whose payload names Ada Lovelace with iat 1516239022 and exp 1516242622. The third segment decodes as the ASCII string not-a-real-signature— not an HMAC over the first two parts.

3 segments · two '.' · JWS (RFC 7515)

Compact JWE: five segments, claims stay ciphertext

RFC 7516 §9: JWEs have five base64url segments separated by four periods — JWE Protected Header, Encrypted Key, Initialization Vector, Ciphertext, Authentication Tag. Direct encryption (alg dir) still uses five parts; the encrypted-key segment may be empty. Appendix A.1's protected header encodes as eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ {"alg":"RSA-OAEP","enc":"A256GCM"}. That first segment is readable JOSE, not the claims. Five segments are rejected here before any decrypt. Nested JWT (a JWS inside a JWE, or the reverse) is not unwrapped.

5 segments · four '.' · JWE (RFC 7516) · rejected

Decode is not verify

Header and payload are encoding, not a secret

After an optional leading Bearer prefix is stripped and whitespace is removed, Decode (a button, not as-you-type) splits on .. Header and payload become bytes via base64url, then UTF-8 with new TextDecoder('utf-8', { fatal: true }), then JSON.parse. Pretty-print is JSON.stringify(..., null, 2). Both must be JSON objects — an array or a primitive fails. The third segment is shown as the original base64url text. RFC 7515 validation (HMAC-SHA-256, RSASSA, ECDSA, or JWKS) never runs. alg is printed from the header, not checked against a key.

sample iat 1516239022 → 2018-01-18T01:30:22.000Z

alg none still has three segments

RFC 7515 Appendix A.5 / RFC 7519 §6: an Unsecured JWS/JWT uses {"alg":"none"} (eyJhbGciOiJub25lIn0) and an empty signature, so the compact form ends with a period. RFC 7518 §3.6: recipients MUST verify that the signature octets are empty, and implementations MUST NOT accept Unsecured JWSs by default. This page still requires the trailing dot (two segments are rejected) and still does not treat that object as valid.

NumericDate is display only

iat / exp / nbf are Unix seconds, not a verdict

RFC 7519 NumericDate is a JSON number of seconds since 1970-01-01T00:00:00Z, leap seconds ignored (IEEE Std 1003.1; each day is 86400 of those seconds). When iat, exp, or nbf is a finite number, the decoder shows new Date(n * 1000).toISOString(). The sample exp 1516242622 is 2018-01-18T02:30:22.000Z. That UTC string is not compared to the clock. A past exp does not make the token “invalid” here. RFC 7515 Appendix A.5's exp 1300819380 is 2011-03-22T18:43:00.000Z.

NumericDate seconds × 1000 → toISOString() only

Not a validator, not encryption

Copy writes the pretty-printed header and payload. Load sample fills the compact string and does not run Decode. This is not HMAC verify, not RS256/ES256, not JWKS, not a secret box, not alg allow-listing, not JWE decrypt, and not a Nested JWT unwrap. RFC 7516 JSON Serialization (ciphertext member) and JWS JSON Serialization (payload member) are out of scope.

Inspect a compact JWS in the browser

Three segments only. The signature is shown, never verified. Nothing is uploaded.

Use JWT Decoder →

FAQ

Is every JWT three segments?
No. RFC 7519 is a claims set used as a JWS payload or as JWE plaintext. Compact JWS (RFC 7515) is three base64url segments separated by two periods: header.payload.signature. Compact JWE (RFC 7516 §9) is five segments separated by four periods: protected header, encrypted key, IV, ciphertext, authentication tag. Two segments are rejected. JSON serializations (a payload member vs a ciphertext member) are not accepted here.
Does decoding a JWT verify the signature?
No. Compact JWS header and payload are RFC 4648 §5 base64url of UTF-8 JSON — not ciphertext. Anyone who has the compact string can JSON.parse those objects. Verification is a later RFC 7515 step: HMAC, RSA, or ECDSA over ASCII(BASE64URL(header) || "." || BASE64URL(payload)) with a key. This page never loads a secret, JWKS, or public key and never claims the token is valid.
What does a trailing dot mean?
RFC 7515 Appendix A.5 and RFC 7519 §6 define an Unsecured JWS/JWT: alg is the string none and the JWS Signature is the empty octet sequence, so Compact Serialization ends with a period. RFC 7518 §3.6 says implementations MUST NOT accept Unsecured JWSs by default. Docify still shows the empty third segment and still does not treat that form as valid.
Can this page decrypt JWE or unwrap a Nested JWT?
No. Five segments is compact JWE and is rejected before any decode. The first JWE segment is a readable JOSE header (RFC 7516 Appendix A.1 starts eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ → {"alg":"RSA-OAEP","enc":"A256GCM"}), but the claims stay in the ciphertext segment. Nested JWT (a JWS inside a JWE, or the reverse) is not unwrapped. Nothing is uploaded.

Related