# How I check a JWT when an API login looks broken

A beginner-friendly, step-by-step guide to opening a JWT, reading the claims, checking expiry, and understanding signature verification.

- Date: 2026-09-27
- URL: https://ilham.dev/posts/how-to-read-and-verify-jwt/
- Markdown: https://ilham.dev/posts/how-to-read-and-verify-jwt/index.md
- Tags: jwt, security, api, tools
- Reading time: 7 min


A JWT is one of those strings that looks scary the first time you see it. It often
appears in a login response, browser storage, or an API request header:

```text
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJyb2xlIjoidXNlciJ9...
```

At first glance it looks like random text. The helpful thing to know is that a JWT
is usually three pieces joined with dots:

```text
header.payload.signature
```

The [JWT Parser](/tools/jwt-parser/) opens those pieces and shows the readable JSON
inside. You do not need to install anything or write a script.

This is the way I check a JWT when a login or API request is not behaving as I
expect.

## Before starting: what problem are we trying to solve?

A JWT is commonly used as a temporary proof that a user has logged in. The frontend
sends it to the backend, usually like this:

```http
Authorization: Bearer <token>
```

When something breaks, the symptom is usually one of these:

- the app says I am logged in, but the API returns `401 Unauthorized`;
- the token worked a few minutes ago, but now it does not;
- the wrong user id or role seems to be used;
- staging and production tokens are mixed up;
- someone says “the JWT is invalid” but does not say which part is invalid.

The goal is not to become a JWT expert. The goal is to answer simple debugging
questions:

1. Can the token be opened?
2. Who or what does the token claim to represent?
3. Has it expired?
4. Was it signed with the expected key?
5. Is the app sending it in the right place?

## Step 1: copy the whole token

First, copy the JWT exactly as it appears.

A JWT normally has two dots in it:

```text
xxxxx.yyyyy.zzzzz
```

If you only copy one part, the parser will not be able to read it properly. Be
careful not to include extra text like `Bearer` unless the tool says it accepts it.
If the token comes from an HTTP header, the full header may look like this:

```http
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```

The token is the long part after `Bearer`.

## Step 2: paste it into the JWT Parser

Open [JWT Parser](/tools/jwt-parser/) and paste the token into the input box.

If the token is well formed, the tool will split it into three areas:

- **Header**
- **Payload**
- **Signature**

If it cannot be split, check for the common copy mistakes:

- missing beginning or end of the token;
- spaces inserted into the token;
- quotation marks copied from JSON;
- only the payload copied instead of the full token;
- line breaks added by a terminal or chat app.

## Step 3: read the header

The header tells you what kind of token it is and what signing algorithm it claims
to use. It may look like this:

```json
{
  "alg": "HS256",
  "typ": "JWT"
}
```

The most important field here is `alg`.

Common examples:

- `HS256`: signed with a shared secret;
- `RS256`: signed with a private key and checked with a public key;
- `PS256`: similar to RSA, but with RSA-PSS;
- `ES256`: signed with an elliptic-curve key.

As a beginner, I mostly use this field to answer one question:

> What kind of key do I need if I want to verify the signature?

For `HS256`, I need the secret. For `RS256` and similar algorithms, I need the
public key.

## Step 4: read the payload

The payload is the part that usually explains who the token is for and what it is
allowed to do.

A small payload might look like this:

```json
{
  "sub": "123",
  "email": "user@example.com",
  "role": "admin",
  "iat": 1790467200,
  "exp": 1790470800
}
```

The exact fields depend on the application. There is no single payload shape that
every JWT must use.

Fields I usually look for:

- `sub`: the subject, often the user id;
- `email`: the user email, if the app includes it;
- `role`: the user's role, if the app uses role-based access;
- `iss`: issuer, meaning who created the token;
- `aud`: audience, meaning who the token is meant for;
- `iat`: issued at;
- `nbf`: not valid before;
- `exp`: expires at.

This step often catches simple problems. For example, the token may belong to a
different user, a different environment, or an old login session.

## Step 5: understand the important warning

Reading the payload does **not** mean the token is trusted.

Anyone can decode a JWT payload. Anyone can also create a fake payload that says:

```json
{
  "sub": "1",
  "role": "admin"
}
```

That fake payload may look valid when decoded. The part that prevents people from
changing it safely is the signature.

A simple way to think about it:

- the payload is the message;
- the signature is the seal;
- decoding reads the message;
- verification checks whether the seal still matches.

So the JWT Parser is useful for reading, but the token should only be trusted after
signature verification and application-level checks.

## Step 6: check the expiry time

The most common JWT problem I see is an expired token.

Look for `exp` in the payload:

```json
{
  "exp": 1790470800
}
```

That number is usually a Unix timestamp. It is not easy to read by eye, so copy it
into the [Timestamp Converter](/tools/timestamp-converter/).

Then check:

1. Is the expiry time in the past?
2. Is it in the expected timezone when displayed as local time?
3. Is the token lifetime too short for the flow you are testing?

If `exp` is already past, the backend should reject the token. That is true even if
the signature is valid.

Also check `nbf` if it exists. `nbf` means “not before”. If that time is still in
the future, the token is not valid yet.

## Step 7: verify the signature if you have the key

To verify the signature, you need the right key for the algorithm.

For HMAC tokens such as `HS256`, paste the shared secret. This is usually something
from the backend configuration, environment variables, or test setup.

For public/private key tokens such as `RS256`, paste the public key. It often looks
like this:

```text
-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----
```

After you provide the key, the parser recomputes the signature and compares it with
the signature in the token.

If verification passes, it means:

- the token has not been changed since it was signed;
- the key you entered matches the token;
- the header and payload still match the signature.

If verification fails, common causes are:

- the wrong secret or public key was used;
- the token was copied incorrectly;
- the token was edited;
- the frontend is using a token from another environment;
- the backend changed keys and the token was signed with an old key.

## Step 8: do not stop at “signature valid”

A valid signature is important, but it is not the whole decision.

An application should still check things like:

- is the token expired?
- is the issuer trusted?
- is the audience correct?
- does the user still exist?
- is the role still allowed?
- was the token issued for this app, or for another service?

This is why a token can be cryptographically valid but still rejected by the API.
For example, the signature may pass, but the `aud` value may be for a different
service.

## Step 9: check how the token is sent

If the token itself looks fine, check the request.

Most APIs expect this header:

```http
Authorization: Bearer <token>
```

Common mistakes:

- sending `Bearer` without a space;
- sending the token in the wrong header;
- sending an old token from browser storage;
- forgetting to include the header on one request;
- using a staging token against production API;
- using a production token against local development API.

A JWT can be perfectly valid and still fail if it is sent to the wrong place.

## Step 10: use the right JWT tool for the job

There are three JWT-related tools on this site:

- [JWT Parser](/tools/jwt-parser/) — best for opening and checking an existing token;
- [JWT Encode](/tools/jwt-editor/) — best for creating a test token from header and payload JSON;
- [JWT Expiry Editor](/tools/jwt-expiry-editor/) — best when you only want to adjust `iat`, `nbf`, or `exp`.

If I am debugging a real token, I start with the parser. If I am creating a token
for a test case, I use the editor. If I only need to test expiry behaviour, I use
the expiry editor.

## My beginner checklist

When a JWT problem appears, I go through this checklist:

1. Copy the full token, not just one part.
2. Paste it into [JWT Parser](/tools/jwt-parser/).
3. Confirm the header has the algorithm I expected.
4. Read the payload and check the user id, email, role, issuer, and audience.
5. Convert `exp`, `iat`, and `nbf` with the [Timestamp Converter](/tools/timestamp-converter/).
6. If I have the secret or public key, verify the signature.
7. Check that the API request sends `Authorization: Bearer <token>`.
8. If it still fails, compare environment: local, staging, production.

Most JWT issues are not mysterious. They are usually an expired token, a copied
partial token, the wrong environment, or a key mismatch.

The most useful distinction is this:

> Decoding shows what the token says. Verification checks whether the token should
> be believed.
