JWT Explained: What Is Inside a Token and How It Is Verified
A JSON Web Token is three Base64url strings joined by dots. Here is what each one holds, why anyone can read it, what the signature proves, and how to find out why a token expired.

Key takeaways
- A JWT is header.payload.signature. The first two parts are Base64url text that anyone can read without a key.
- Decoding shows you the claims. Only checking the signature with the right key shows they are genuine.
- HS256 uses one shared secret to sign and to check. RS256 signs with a private key and checks with a public key, which is safer across services.
- Never put passwords or personal data in the payload. Always set an expiry. Reject tokens whose header says alg none.
- To debug an expired token, turn the exp claim from Unix seconds into a date. Then compare it with the server clock in UTC.
A JSON Web Token (JWT) is three pieces of text joined by dots. The first is a header that says how the token is signed. The second is a payload of claims, such as who the user is and when the token ends. The third is a signature that proves the first two parts were not changed. The header and payload are only encoded, not locked, so anyone who holds the token can read them. The signature is what makes a token trustworthy. Only someone with the right key can check it. This guide walks through each part and the claims you will meet. It covers the two common signing methods and the mistakes that turn JWTs into security holes. It ends with a simple routine for debugging a token that gets rejected.
The three parts of a token
Here is a token as you will see it in an Authorization: Bearer header, a cookie or a log line:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Split it at the two dots. Decode the first two parts. You get plain JSON.
Header
{"alg":"HS256","typ":"JWT"}
alg names the signing method. typ says this is a JWT. Some tokens add kid, a key ID. It tells the checker which public key to fetch.
Payload
{"sub":"1234567890","name":"John Doe","iat":1516239022}
The payload is a flat JSON object of claims. A claim is a fact about the user or the token. Some names are standard. The rest are whatever the app chose to add. Here sub names the subject. name is a custom claim. iat records when the token was issued, as a Unix timestamp (seconds since 1 January 1970). That value is 18 January 2018 at 01:30:22 UTC.
Signature
The third part is not JSON. For HS256 it is worked out like this:
HMAC-SHA256(base64url(header) + "." + base64url(payload), secret)
The result is 32 bytes. Base64url turns that into the 43 characters you see. Change one character of the payload and the signature no longer matches. The checker then rejects the token. SHA-256 on its own gives the same 32-byte length. You can see what one looks like by hashing any text in the Hash Generator. HMAC adds a secret key to the hashing. So only someone who knows the key can make a valid result.
Base64url, not quite Base64
JWTs use Base64url. It is a small change to Base64 that makes it safe in URLs and headers. The changes are small, but they matter when you decode by hand:
| Standard Base64 | Base64url | |
|---|---|---|
| Character 62 | + | - |
| Character 63 | / | _ |
| Padding | = at the end to a multiple of 4 | Omitted |
| Safe in a URL as-is | No | Yes |
A part like eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 decodes in any Base64 tool. It has no - or _ and needs no padding. A signature such as SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c is different. Swap the underscore for a slash and add the padding back. Only then will a standard decoder accept it. The Base64 Encoder and Decoder handles the URL-safe version. The JWT Decoder splits and decodes all three parts in one step. It also shows the expiry as a readable date.
A part that starts with eyJ is a clue. Those are the Base64 bytes of {", the start of a JSON object. If you see eyJ in a log, a cookie or a query string, it is almost surely a JWT. It is also almost surely readable.
Standard claims
RFC 7519 sets aside seven short claim names. All are optional. But a token without exp is a token that never dies.
| Claim | Meaning | Type | Example |
|---|---|---|---|
iss | Issuer: who created the token | String or URL | "https://auth.example.com" |
sub | Subject: who the token is about, usually a user ID | String | "user_8841" |
aud | Audience: which service the token is for | String or array | "orders-api" |
exp | Expiry time | Unix seconds | 1793102400 |
nbf | Not before: reject the token until this time | Unix seconds | 1793098800 |
iat | Issued at | Unix seconds | 1793098800 |
jti | JWT ID: a unique ID for the token, used for revoke lists | String | "c2f1-9a3e" |
The time claims are seconds since 1 January 1970 UTC. They are not milliseconds. Say a library writes Date.now() straight into exp. The token then expires around the year 58,000. The Unix Timestamp Converter shows at a glance which unit a value uses. Any other name, such as role, email or tenant_id, is a custom claim. The checker must decide for itself whether to trust it.
Decoding is not verifying
This is the point people most often get wrong. Decoding a JWT means Base64url-decoding two strings. It needs no secret, no key and no network call. That is why a browser tool can do it at once. It tells you what the token says. It tells you nothing about whether the token is genuine.
Verifying means working out the signature again from the exact header and payload, using the correct key. You then compare it, byte for byte, with the third part. Only then do you know the issuer wrote the claims and nobody has edited them. A checker must also test a few more things. The exp time must be in the future. The nbf time must be in the past. The iss must be the issuer you expect. The aud must name your service. A token may pass the signature check but be meant for a different API. You should still reject it.
Anyone can make a token that decodes to {"sub":"admin","role":"superuser"}. Suppose your server reads the payload and acts on it without checking the signature. Then that person is now your superuser. Every JWT library has a decode function and a verify function. Production code must call the second one.

HS256 versus RS256
These are the two methods you will meet most often. They differ in who holds the key.
| HS256 | RS256 | |
|---|---|---|
| Type | HMAC with SHA-256, symmetric | RSA signature with SHA-256, asymmetric |
| Signing key | A shared secret | A private key |
| Verifying key | The same shared secret | The matching public key |
| Who can mint tokens | Anyone who can verify them | Only the holder of the private key |
| Signature length | 43 characters | 342 characters for a 2048-bit key |
| Speed | Very fast | Slower, though still thousands per second |
| Best fit | One service issuing and using its own tokens | One issuer, many services that use the tokens; third-party identity providers |
With HS256, every service that checks tokens must hold the secret. So every one of them can also forge tokens. That is fine inside one app. It is a risk across a dozen small services or any third party. RS256 solves this. The login server keeps the private key. It shares the public key, usually as a JWKS document at a well-known URL. Everyone else can check tokens but cannot sign them. ES256 uses elliptic curve keys. It gives the same safety with much shorter signatures. It is fast becoming the default in new systems.
If you use HS256, the secret must be truly random. It must be at least 32 bytes long. A short, guessable secret can be cracked offline from one captured token. The attacker has everything needed to test guesses at full speed.
Common security mistakes
Secrets or personal data in the payload
The user can read the payload. So can every proxy, and anyone who finds the token in a log. Passwords, API keys, card numbers and anything covered by privacy law do not belong there. Put an ID in the token and look up the rest on the server. Sometimes you truly need private claims. Then use JWE, the encrypted sibling of JWT. Do not hope that nobody decodes a signed one.
No expiry, or an expiry measured in months
A token with no exp is valid forever. You cannot recall it without changing the signing key for everyone. The usual pattern is a short-lived access token of 5 to 60 minutes. It is paired with a refresh token that the server can revoke. Long sessions should come from the refresh token, not from a long exp.
Accepting alg none
The spec allows "alg":"none" for unsigned tokens with an empty signature. Older libraries accepted such a token if the header asked for it. An attacker could strip the signature, change alg and edit the payload at will. Modern libraries refuse none by default. Even so, your code should name the methods it accepts. Do not let the header choose.
Letting the header choose the algorithm
Here is a related attack. A service checks RS256 tokens with a public key. Someone sends a token whose header says HS256. A naive library uses the public key as the HMAC secret. But that key is public. So the attacker can sign with it and get in. The defence is the same as before. The checker picks the method and the key. The token never does.
Skipping the audience and issuer checks
A valid token from your identity provider for the billing dashboard should not open the admin API. Check aud and iss on every request.
Storing tokens where scripts can read them
A token in localStorage can be read by any script on the page, even an injected one. An HttpOnly, Secure, SameSite cookie cannot. If your design allows it, prefer the cookie.

How to debug an expired token
The error says token expired or jwt expired. Or it simply returns 401. The token looked fine a moment ago. Work through these steps in order.
- Decode the token in the JWT Decoder. Read
exp,iatandnbf. - Convert the values. Say
iatis1793098800andexpis1793102400. The gap is 3,600 seconds, so the token lived for one hour. Some gaps are odd numbers, such as 2,592,000. The Time Converter turns seconds into hours or days, and that one is 30 days. Converting the two values gives 09:00:00 UTC and 10:00:00 UTC on 27 October 2026. - Compare with the server clock in UTC. Do not use your laptop clock or local time. Say the server shows 10:00:05 UTC. The token ended five seconds ago, and the client just needs to refresh. Say the server shows 09:30 UTC and still rejects the token. Then expiry is not the problem. Check
nbf, the audience, or whether the signing key was changed. - Check the units. An
expof1793102400000is in milliseconds. Many checkers read it as seconds. The token then looks valid for 56,000 years. Others reject it as out of range. - Check for clock skew. Say tokens expire the moment they are issued. Then the issuing and checking servers disagree about the time. Make sure both run NTP, a service that keeps clocks right. Allow a small leeway of 30 to 60 seconds in the checker. Most libraries let you set this.
- Check the time zone of whoever made the token. A server may use local time in a zone ahead of UTC. The
expit writes is then already past elsewhere. All JWT times are UTC by rule. The fix belongs in the issuing code.
Example. A support ticket says logins fail every morning for the first few minutes. The decoded tokens show iat values about 90 seconds in the future, as seen from the API server. The clock on the auth server had drifted. The checker applied nbf strictly, so it rejected every token until real time caught up. The team restored NTP on the auth server and set a 60-second leeway. That fixed it.
Where to go next
Paste any token into the JWT Decoder. You will see the header, the claims and a readable expiry without sending the token anywhere. For a single part or an odd token, the Base64 Encoder and Decoder handles the URL-safe version. Use the Unix Timestamp Converter to turn exp and iat into dates. It also helps you spot seconds mixed up with milliseconds. Use the Hash Generator to see what a SHA-256 digest looks like when you think about signature lengths.
Frequently asked questions
Can anyone read a JWT?
Yes. The header and payload are Base64url-encoded JSON. That is an encoding you can reverse, not encryption. So anyone who holds the token can read every claim without a key. The signature only stops people from changing the contents without being caught. Never put passwords, secrets or private personal data in a JWT payload. If you need privacy, use an encrypted format such as JWE.
What is the difference between decoding and verifying a JWT?
Decoding turns the two Base64url parts back into JSON so you can read the claims. It needs no key. Verifying works out the signature again from the header and payload with the right secret or public key. It then compares the result with the token's third part and checks the expiry, issuer and audience. Only a verified token can be trusted. A decoded one just shows what it claims.
Is HS256 or RS256 better?
RS256 is the safer choice when more than one service checks tokens. Those services only need the public key, and they cannot forge tokens with it. HS256 uses one shared secret to sign and to check, so every checker could also make tokens. It is fine for one app that issues and uses its own tokens, if the secret is random and at least 32 bytes long.
How long should a JWT last?
Access tokens usually last 5 to 60 minutes. A signed token cannot be recalled once issued unless you change the key for everyone. Longer sessions are kept alive with a separate refresh token. The server stores that token and can cancel it. A token without an exp claim never expires, which is a serious weakness. So always set one and always check it.
Why does my JWT say it is expired when it was just issued?
The usual cause is clock skew. The server that issues the token and the server that checks it disagree about the time by more than the checker allows. Make sure both run NTP, and allow a leeway of 30 to 60 seconds. Other causes are an exp written in milliseconds instead of seconds, or an issuing server that used local time instead of UTC.
Try the tools from this guide
Related guides
JSON Formatting and Validation: Syntax Rules, Common Errors and Fixes
Why a file that looks fine will not load. The ten mistakes behind most broken JSON, how to escape text, when to minify, and how to move data between CSV and JSON.
Read the guideImage Sizes for Social Media and the Web in 2026: The Reference Table
One reference table of pixel sizes and aspect ratios for every major platform, plus the safe zones, the retina 2x rule and the file-size targets that keep images sharp and fast.
Read the guideWhat a Mortgage Payment Really Includes, With Worked Numbers
The loan repayment is only part of what leaves your account each month. Here is every component, a worked example, and how the deposit and term change the total.
Read the guide

