If you have spent any time working on modern web applications, you have definitely run into JSON Web Tokens. People usually pronounce it as “jot” (because saying the individual letters J-W-T over and over gets old fast). They have become the industry standard way to send information between a frontend client and a backend API server.
But because a JWT looks like a giant, messy string of random characters, they are almost impossible to inspect by eye. When your authentication fails, or when a user gets logged out for no reason, you need to look inside the token to see what went wrong.
In this guide, we will look at how JSON Web Tokens are structured, what claims they carry, and why traditional Base64 decoders will crash when you feed them a JWT. We will also write code to decode them manually and show you how to inspect them safely without leaking your private information to shady online servers.
What exactly is a JSON Web Token?
A JSON Web Token (defined in RFC 7519) is a compact and self-contained way of transmitting information between parties as a JSON object. Because the information is digitally signed, it can be verified and trusted.
Stateless authentication is the primary use case for JWTs. In the old days, when a user logged in, the server would create a session ID and save it in a database or in-memory store. The server would send that session ID to the client as a cookie. Every time the client made a request, the server had to lookup that session ID in the database to verify the user.
With JWTs, we do things differently. When the user logs in, the server creates a token containing the user’s ID, their name, and their permissions. The server signs this token with a secret key and sends it to the client. The client stores the token (usually in local storage or a secure cookie) and sends it with every API request. The backend server does not need to look up anything in a database. It simply verifies the signature. If the signature is valid, the server trusts the data inside the token.
How is a JWT structured?
A JWT is not just a random string of characters. It is made of three distinct parts, separated by periods:
- The Header
- The Payload
- The Signature
So, a typical token looks like this: header.payload.signature
Let us break down what each of these sections actually does.
1. The Header
The header is the first part of the token. It tells the server how to handle the token. It typically contains two pieces of information: the type of token (which is almost always JWT) and the signing algorithm being used. Common algorithms include HS256 (HMAC using SHA-256) and RS256 (RSA signature using SHA-256).
Here is what the decoded header JSON looks like:
{
"alg": "HS256",
"typ": "JWT"
}
2. The Payload
The payload is the middle part of the token and it contains the actual data. This data is referred to as claims. Claims are statements about the user and any extra metadata the server needs.
Here is an example of a decoded payload JSON:
{
"sub": "user_987654",
"name": "Jane Developer",
"admin": true,
"exp": 1774896000
}
There are three types of claims:
- Registered claims: These are predefined claims that are recommended for interoperability. They include
iss(issuer),exp(expiration time),sub(subject), andaud(audience). - Public claims: These are claims that you can define yourself, but should be defined in a way that avoids collisions (like using a URI namespace).
- Private claims: These are custom claims created to share information between your client and server, like roles or email addresses.
3. The Signature
The signature is the security guard of the token. To create the signature, the server takes the encoded header, the encoded payload, and signs them using the algorithm specified in the header along with a secret key.
If anyone changes even a single letter in the header or payload, the signature will no longer match. The server will know the token has been tampered with and will reject it.
What happens if you corrupt a JWT?
A common point of confusion is that developers think JWTs are encrypted. They are not.
By default, a standard JWT (technically called a JSON Web Signature or JWS) is only signed. The data inside is simply encoded, meaning anyone who gets hold of the token can read your user IDs, emails, and permissions. If you put sensitive information like passwords or credit card numbers in a JWT payload, you are making a massive security mistake.
If you corrupt a JWT, say by changing the payload to make yourself an administrator, the server’s signature check will fail.
Here is what happens when a backend server receives a token:
- It splits the token into the three parts: header, payload, and signature.
- It takes the header and payload, and runs them through the algorithm (like HS256) using the server’s secret key.
- It compares the newly generated signature with the signature that came with the token.
- If they match, the token is valid. If they do not match, the server throws an error and rejects the request.
If you need the data to be completely hidden from the client, you must use JSON Web Encryption (JWE), which encrypts the payload, or simply keep sensitive data out of your tokens.
What are the most common JWT claims you will see?
When you decode a JWT, you will see a lot of three letter keys. These are the registered claims defined by RFC 7519. Here are the ones you will run into most often:
- iss (Issuer): Identifies who created and sent the token. This is often the URL of your authentication server.
- sub (Subject): Identifies the entity the token belongs to. This is usually the unique user ID in your database.
- aud (Audience): Identifies who the token is intended for. The receiving server will check this to make sure the token was meant for them.
- exp (Expiration Time): The exact date and time when the token becomes invalid. This is written as a Unix timestamp (seconds since January 1, 1970).
- nbf (Not Before): The exact time before which the token must not be accepted.
- iat (Issued At): The time when the token was created.
- jti (JWT ID): A unique identifier for the token. This can be used to prevent replay attacks by ensuring a token is only used once.
Understanding these claims is critical when debugging authentication issues. If a user is getting a forty-one unauthorized error, the first thing you should check is the exp claim to see if their token has expired.
Why does standard Base64 decoding fail on JWTs?
If you try to decode a JWT by splitting the parts and pasting them into a standard Base64 decoder, you will often get errors or corrupted text.
This happens because JWTs do not use standard Base64 encoding. They use Base64URL encoding.
Here are the key differences between the two formats:
- Standard Base64 uses the characters
+and/. These characters have special meanings in URLs, which makes standard Base64 unsafe to use in query parameters or headers. Base64URL replaces+with-(minus) and/with_(underscore). - Standard Base64 uses
=characters at the end of the string as padding to make sure the length is a multiple of four. Base64URL strips out all padding characters because=is also unsafe in URLs.
If your decoder does not replace these characters and restore the padding, it will fail to parse the token.
How do you decode a JWT manually in JavaScript?
To decode a JWT client side in JavaScript, you have to write code that translates the URL safe characters back to standard Base64 and adds the correct padding before running it through the decoding function.
Here is a complete, helper function that does this in vanilla JavaScript without any external libraries:
function decodeJwt(token) {
try {
// Split the token into its three parts
const parts = token.split(".");
if (parts.length !== 3) {
throw new Error("Invalid JWT format: Must have three parts separated by dots");
}
const headerPart = parts[0];
const payloadPart = parts[1];
// Helper function to decode a Base64URL string
function base64UrlDecode(str) {
// Replace URL-safe characters back to standard Base64 characters
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
// Restore the removed padding characters (=)
const pad = base64.length % 4;
if (pad === 2) {
base64 += "==";
} else if (pad === 3) {
base64 += "=";
} else if (pad === 1) {
throw new Error("Invalid Base64 string padding");
}
// Decode the Base64 string to a UTF-8 string
const binaryString = atob(base64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
const decoder = new TextDecoder("utf-8");
return JSON.parse(decoder.decode(bytes));
}
// Decode the header and payload
const decodedHeader = base64UrlDecode(headerPart);
const decodedPayload = base64UrlDecode(payloadPart);
return {
header: decodedHeader,
payload: decodedPayload
};
} catch (error) {
return {
error: error.message
};
}
}
// Example usage:
const sampleToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
console.log(decodeJwt(sampleToken));
This code is safe, runs entirely in memory, and handles special unicode characters properly (which standard atob fails to do on its own).
How do you verify a JWT signature in Node.js?
Decoding a JWT is only half the battle. If you are writing backend code, you must verify the signature before you trust any of the payload data.
Here is a native Node.js example showing how to verify an HS256 (HMAC SHA-256) signature without installing any NPM packages. It uses Node’s built in crypto module.
const crypto = require("crypto");
function verifyHs256Signature(token, secret) {
const parts = token.split(".");
if (parts.length !== 3) {
return false;
}
const [headerB64, payloadB64, signatureB64] = parts;
// Recreate the data string that was originally signed
const signedInput = `${headerB64}.${payloadB64}`;
// Recreate the HMAC using the secret key
const hmac = crypto.createHmac("sha256", secret);
hmac.update(signedInput);
// Generate the signature buffer
const calculatedSignatureBuffer = hmac.digest();
// Convert the token signature back to a buffer
// We must restore padding and replace characters for comparison
let base64Sig = signatureB64.replace(/-/g, "+").replace(/_/g, "/");
const pad = base64Sig.length % 4;
if (pad === 2) base64Sig += "==";
if (pad === 3) base64Sig += "=";
const tokenSignatureBuffer = Buffer.from(base64Sig, "base64");
// Use timingSafeEqual to protect against timing attacks
if (calculatedSignatureBuffer.length !== tokenSignatureBuffer.length) {
return false;
}
return crypto.timingSafeEqual(calculatedSignatureBuffer, tokenSignatureBuffer);
}
// Example usage
const key = "super-secret-key-do-not-share";
const testToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
const isValid = verifyHs256Signature(testToken, key);
console.log("Is signature valid?", isValid);
Using crypto.timingSafeEqual is critical. It makes sure that comparisons take the same amount of CPU time regardless of where a mismatch occurs, which stops malicious actors from guessing your signature byte-by-byte.
Why are online JWT tools a security nightmare?
When a token fails to verify, the easiest thing to do is copy it, search for “online JWT decoder,” and paste it into the first result.
This is a massive security risk.
When you paste a token into a site that processes the token on their servers, you are sending them:
- Valid session credentials for your users.
- User names, emails, and roles.
- Technical metadata about your application architecture.
If a bad actor intercepts or logs these tokens, they can hijack active user sessions without ever needing their passwords. For corporate environments or applications dealing with personal user data, pasting live tokens into third party online tools is a serious data breach risk.
How does the TextSorter JWT Decoder keep you safe?
Our free JWT Decoder is designed from the ground up to solve this security issue.
Here is why it is different:
- True client-side processing: Your tokens are never sent to our servers. All decoding and parsing happens entirely within your browser’s JavaScript environment.
- Local signature verification: You can paste your secret key to check signatures locally. The tool uses the Web Crypto API built directly into your browser, meaning your secret key never leaves your machine.
- Readable payload inspector: The tool automatically formats JSON objects, highlights expiration issues in real time, and translates unix timestamps into human readable local dates.
To inspect your tokens securely, visit the TextSorter JWT Decoder.
Frequently Asked Questions about JSON Web Tokens
What is the difference between a JWT and a session cookie?
A session cookie is a reference ID pointing to data stored on the server. The server must look up this ID on every request. A JWT is a self-contained token. The server does not store anything; it simply validates the token’s cryptographic signature to trust the data inside it.
Can a client modify a JWT?
Yes, a client can modify the header and payload of a JWT easily because they are only Base64URL encoded. However, doing so will invalidate the signature. When the server tries to verify the modified token, the signature check will fail, and the request will be rejected.
How long should a JWT be valid?
Because JWTs are stateless and hard to revoke, they should have a short lifespan. A common practice is to set the expiration (exp) to fifteen minutes for access tokens, and use a secure, HTTP-only refresh cookie to get new access tokens when they expire.
What is the difference between HS256 and RS256?
HS256 is a symmetric algorithm, meaning the same secret key is used to both sign and verify the token. Both the authentication server and the API server must know this secret. RS256 is an asymmetric algorithm. It uses a private key to sign the token and a public key to verify it. This is useful when the API server is managed by a different team who should not have access to the signing key.
How do you revoke a JWT?
Because JWT verification is stateless, you cannot easily revoke a token once it is issued. To handle logouts or security bans, you can maintain a database blacklist of revoked token IDs (jti) that are checked on every request, or simply wait for the token to expire.