JWT Tokens: Understanding Base64 Encoding
JSON Web Tokens (JWTs) are the backbone of modern web authentication, used by virtually every OAuth 2.0 and OpenID Connect implementation. Yet many developers treat JWTs as opaque blobs without realizing that two-thirds of every token is just Base64url-encoded JSON readable by anyone. This guide demystifies the structure of JWTs, shows you exactly how Base64url encoding is applied, demonstrates how to decode any JWT in seconds, and explains what the cryptographic signature actually protects — and what it does not.
The Three-Part Structure of a JWT
A JWT is a string of the form xxxxx.yyyyy.zzzzz — three Base64url-encoded sections separated by dots. Each section plays a distinct role. Section 1 — Header: a JSON object that describes the token itself. It specifies the token type (almost always JWT) and the signing algorithm. Example: {"alg":"HS256","typ":"JWT"}. Common algorithms include HS256 (HMAC-SHA256, symmetric — same key signs and verifies), RS256 (RSA-SHA256, asymmetric — private key signs, public key verifies), and ES256 (ECDSA-SHA256, asymmetric). The algorithm choice matters enormously for security — the infamous alg:none attack exploits servers that accept unsigned tokens when the header claims no algorithm is used. Section 2 — Payload: a JSON object containing the claims — assertions about the subject of the token and metadata about the token itself. Standard registered claims include: sub (subject, typically a user ID), iss (issuer, typically the auth server URL), aud (audience, the intended recipient), exp (expiration time, as a Unix timestamp), iat (issued at time), and nbf (not before time). Applications add custom claims as needed: roles, permissions, email addresses, tenant IDs. Section 3 — Signature: the cryptographic value that protects the header and payload from tampering. Computed by signing Base64url(header) + "." + Base64url(payload) with the private or secret key specified by the algorithm. The signature is binary data, also Base64url-encoded. Decoding it gives raw bytes that are not human-readable without the key. The key insight: the header and payload are readable by anyone. Only the signature provides integrity protection. If your payload contains sensitive data, you need JWE (JSON Web Encryption) — a JWT variant that actually encrypts the payload.
Decoding a JWT Step by Step
Decoding a JWT manually requires only a Base64url decoder and a JSON parser. Here is the step-by-step process. Step 1: split the token at the dot separators. You will have three strings. For example, the token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwiZXhwIjoxNzE4MDAwMDAwfQ.SomeSignature splits into three parts at each period. Step 2: take the first part (header) — eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 — and normalize it for standard Base64 decoding. Replace all - with + and all _ with /. Add = padding characters until the length is a multiple of 4. Step 3: decode the normalized string with any Base64 decoder. The WikiPlus Base64 tool works for this. The decoded output is the JSON header: {"alg":"HS256","typ":"JWT"}. Step 4: repeat for the second part (payload). The decoded JSON shows you all the claims: subject, expiry time, issuer, and any custom claims. Step 5: convert the exp value from Unix timestamp to a human-readable date to check if the token has expired. A Unix timestamp of 1718000000 equals some point in 2024 — compare it to the current time. The third part (signature) will decode to binary bytes. Do not attempt to interpret them as text. The signature is only meaningful to the server that validates it — for debugging purposes, the header and payload are what you need.
What the Signature Protects and What It Does Not
The JWT signature ensures two properties: integrity (the header and payload have not been modified since the token was issued) and authenticity (the token was issued by someone who possesses the signing key). If an attacker modifies any byte in the header or payload — changes a user ID, elevates a role, extends an expiry timestamp — the signature will no longer match the modified content. A properly validating server will reject the token. This is the core security guarantee of a JWT. However, the signature provides no confidentiality. The payload is Base64url-encoded, which means anyone who intercepts the token can decode and read all of its contents immediately. For this reason, JWTs should never contain sensitive personal data such as passwords, social security numbers, credit card numbers, or private medical information. Another important limitation: the signature does not prevent replay attacks on its own. A valid JWT remains valid until its exp (expiry) time regardless of whether the user has logged out or the session has been revoked. If a token is stolen (from local storage, a log, a URL query string), the attacker can use it until it expires. Mitigations include short expiry times (15 minutes for access tokens), token revocation lists (which eliminate the stateless benefit of JWTs), or binding tokens to specific IP addresses or devices. Finally, the alg field in the header is used by many JWT libraries to determine which verification algorithm to use — which is why setting alg:none in a forged token could bypass signature verification in libraries that blindly trust the header's algorithm claim. Always configure your JWT library to only accept specific, expected algorithms.
Practical JWT Debugging Workflow
When a JWT-based authentication flow breaks, a structured debugging approach saves significant time. Here is the workflow used by experienced backend developers. First, inspect the token structure. Copy the JWT from your browser's Application tab (cookies, localStorage) or from the network request Authorization header. Verify it has exactly two dots separating three sections. A malformed token with extra or missing dots will fail before signature validation. Second, decode the payload. Use the WikiPlus Base64 tool, a dedicated JWT debugger, or a one-liner in your shell: echo '<payload_section>' | base64 -d (Linux/macOS) or the equivalent Node.js command. Confirm the sub, iss, aud, and exp fields are correct. An exp value in the past means the token has expired and the client needs to refresh it. Third, verify the algorithm. The alg field in the header should match what your server expects. If your server is configured for RS256 but the token claims HS256, that mismatch will cause verification failures. Fourth, check the audience claim. If your token has an aud claim and your server validates it, the aud value must match the configured expected audience exactly. A common mistake is issuing tokens with aud: ["https://api.example.com"] from a development environment but validating against a production audience value. Fifth, examine clock skew. The nbf (not before) and exp fields depend on system clocks being synchronized. If the issuing server and validating server have clocks out of sync by more than a few seconds, tokens may be rejected as expired or not-yet-valid. Most JWT libraries allow a small tolerance — typically 60 seconds — to handle this.
Frequently Asked Questions
- Can anyone read the contents of a JWT without the signing key?
- Yes. The header and payload sections of a standard JWT are only Base64url-encoded — not encrypted. Anyone with the token string can decode and read all claims in the payload without the signing key. The key is only needed to verify the signature and confirm the token has not been tampered with. Never store sensitive data in a JWT payload unless you are using JWE (JSON Web Encryption), which actually encrypts the payload.
- How do I check if a JWT has expired without a library?
- Decode the payload section by splitting the JWT at dots, taking the second part, Base64url-decoding it, and parsing the resulting JSON. The exp field contains a Unix timestamp (seconds since January 1, 1970 UTC). Compare it to the current Unix timestamp. In JavaScript: Date.now() / 1000 gives the current time in seconds. If exp is less than the current time, the token has expired. Most JWT libraries do this check automatically, but inspecting the raw value is useful during debugging.
- What is the difference between Base64 and Base64url in JWTs?
- Standard Base64 uses + and / characters and adds = padding. JWTs use Base64url, which replaces + with - and / with _ to make tokens safe for use in URL query strings, HTTP headers, and cookies without percent-encoding. Base64url also typically omits the trailing = padding characters because the token structure (three dot-separated sections) makes padding redundant. When decoding JWT sections with a standard Base64 decoder, you must substitute + and _ back before decoding and add padding if needed.