Your API is returning 401s. The frontend says the token looks fine. The backend log says it expired eight minutes ago. Now you need to actually look inside the token.
The instinct is to copy the raw JWT string and paste it into jwt.io or a similar online decoder. For a throwaway staging token generated from seeded test data, that might be acceptable. For a production token carrying a real user's sub, email, or session-bound claims, it is a meaningful risk. That token is not just a string of characters. It is proof of authentication, and in many systems it is sufficient on its own to impersonate a user until expiry.
This guide walks through inspecting JWTs correctly during development: what you are actually looking at when you decode a token, how to extract and read claims without sending anything to a third-party service, and what the common debugging pitfalls look like in practice.
What a JWT Actually Is
A JSON Web Token is three base64url-encoded segments separated by dots. The format is defined in RFC 7519, which is worth bookmarking as the authoritative reference.
Header. The first segment decodes to a JSON object identifying the token type and signing algorithm. For example:
{
"alg": "RS256",
"typ": "JWT"
}
Payload. The second segment contains the claims. Claims are statements about an entity, typically the user, plus metadata the server needs for authorization decisions. Standard registered claim names are defined in the RFC: iss (issuer), sub (subject), aud (audience), exp (expiration time), nbf (not before), iat (issued at), jti (JWT ID). Most real-world tokens also include private claims specific to the application.
Signature. The third segment is the cryptographic signature over the first two parts. This is what prevents token forgery. Decoding a JWT gives you the header and payload in readable form. It does not verify the signature. Those are completely separate operations.
This distinction matters in debugging. You can read a token's claims at any time without the signing key. Verifying whether the token was legitimately issued by your auth server requires the key and is a server-side operation, not a debugging step.
Base64url encoding is a URL-safe variant of base64. It uses - and _ instead of + and /, and omits padding characters. The reason the payload looks like gibberish in a raw HTTP response is that the browser or terminal is not decoding the base64 segments.
The MDN Authorization header documentation covers how JWTs are transmitted in the Authorization: Bearer <token> format, which is the most common pattern for API auth.

Photo by Yash Maramangallam on Pexels
Step-by-Step: Inspecting a JWT During Debugging
Step 1: Capture the Token from the Request
The token lives in the Authorization header on every authenticated request. There are two common ways to grab it quickly.
From the browser's Network tab. Open DevTools, go to the Network panel, click any authenticated request, and look at the Request Headers section. The value after Bearer is the token. You can copy it directly.
From a curl command. If you are testing against a local server or staging environment, you can capture the raw request with verbose output:
curl -v -H "Authorization: Bearer <token>" https://api.example.com/me
The -v flag prints request and response headers to stderr, so you can confirm exactly what the server received.
If you are writing automated tests and want to inspect tokens inline without leaving the terminal, you can pipe the base64url-encoded payload segment directly to a decoder:
# Extract and decode the payload (second segment)
TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsImVtYWlsIjoiamFuZUBleGFtcGxlLmNvbSIsImV4cCI6MTc0MzkwMDAwMH0.signature"
echo $TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null
Note: base64 -d on macOS may require base64 -D. The 2>/dev/null suppresses the padding warning that base64 sometimes emits for base64url-encoded input without padding characters.
Step 2: Read the Claims in the Payload
Once decoded, the payload for a typical user-auth token looks something like this:
{
"sub": "user_123",
"email": "jane@example.com",
"aud": "https://api.example.com",
"iss": "https://auth.example.com",
"iat": 1743813600,
"exp": 1743900000,
"jti": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"roles": ["editor", "viewer"]
}
The fields to focus on immediately when debugging a 401:
-
exp: Unix timestamp for expiry. If the current time is past this value, the token is expired regardless of what the client thinks. -
aud: The intended audience. If the token was issued forapi.example.combut you are hittingadmin.example.com, the audience mismatch will cause a rejection. -
iss: The issuer. Mismatches here are common after migrations or when multiple auth services are running in parallel. -
iat: When the token was issued. Useful for confirming the token was freshly minted and not replayed from a cached response.
For inspection during development, an online JWT decoder that runs client-side keeps your tokens local. EvvyTools' decoder shows the expiry countdown, explains each standard claim, and lets you compare two tokens side by side, which is useful when debugging the difference between a working token and a broken one.
"When I am tracing an auth bug on a client project, the first thing I do is look at the raw claims. Nine times out of ten the issue is an expired token, a wrong audience, or a scope that was not requested. The token tells you everything if you bother to read it." - Dennis Traina, 137Foundry
Step 3: Check Expiry Without Trusting the Server
The exp claim is a Unix timestamp in seconds. To convert it manually:
// In Node.js or browser console
const exp = 1743900000;
const expDate = new Date(exp * 1000);
console.log(expDate.toISOString()); // 2026-04-05T12:00:00.000Z
// Check if it is still valid
const isValid = Date.now() < exp * 1000;
console.log(isValid ? 'Token is valid' : 'Token is expired');
This is intentionally low-tech. If you are debugging a live issue, running three lines in a browser console is faster than anything else. The point is to verify expiry on your machine, independent of any server-side behavior, so you know whether the problem is an expired token or something else entirely.
If there is clock skew between the client and the auth server, iat and exp will look off relative to your system time. This is a real production issue, not a contrived edge case. Auth libraries typically allow a small leeway (commonly 30-60 seconds) to account for drift. If you see a token that expired 12 seconds ago on a request that just failed, clock skew is the first thing to investigate.

Photo by cottonbro studio on Pexels
Security Considerations When Working With JWTs
Never Use a Third-Party Online Decoder for Production Tokens
This deserves to be said plainly: any token that contains real user data, is tied to a live session, or was issued by a production auth server should never be pasted into an online tool that sends the data to a remote server. This includes jwt.io, regardless of how widely trusted it is in the community. The risk is not that jwt.io is malicious. The risk is that the token is a credential, and credentials should not travel outside the systems that need them.
The OWASP JSON Web Token Cheat Sheet covers this and related best practices in depth. It is Java-focused in places but the principles apply across all stacks.
Use client-side tools, browser console one-liners, or local scripts for production token inspection.
Understand the Difference Between RS256 and HS256
The alg field in the header determines how the token is signed and how it must be verified.
HS256 is HMAC with SHA-256. The same secret is used to both sign and verify. This means any party that can verify tokens can also forge them if they obtain the secret. It is appropriate for systems where the auth server and the resource server are the same application or are fully trusted with each other.
RS256 is RSA with SHA-256. The auth server signs with a private key. Resource servers verify with the public key. The private key never leaves the auth server. This is the correct choice when you have multiple services consuming tokens from a central auth provider.
A well-known attack vector is alg: none, where a malformed token claims it requires no signature verification. Libraries that do not explicitly reject this will validate any token. Always configure your JWT library to require a specific algorithm and reject any token that does not match.
Short Expiry and Refresh Tokens Are Not Optional
A token with a 30-day expiry is a 30-day credential if it is ever leaked or stolen. The standard pattern is short-lived access tokens (15 minutes to 1 hour) paired with longer-lived refresh tokens stored in httpOnly cookies. The access token travels in every API request. The refresh token is used only to obtain new access tokens and is never exposed to JavaScript.
Token replay attacks, where an intercepted token is reused by a different client, are mitigated by short expiry times and by binding the token to a specific audience or session identifier. The jti claim exists precisely for this: if your auth server records issued jti values, a replayed token can be rejected even if it has not expired.
Connecting Auth Debugging to the Broader Security Stack
Token handling is one layer of web application security. For the configuration layer, what headers your server should be returning to every browser, this walkthrough on HTTP security headers covers what most teams are missing. Missing headers like Strict-Transport-Security, Content-Security-Policy, and X-Frame-Options are common in applications that otherwise have solid auth implementations.
Further Reading
These are useful references for going deeper on any part of what is covered above:
- JWT.io Introduction - the canonical visual breakdown of JWT structure, useful for onboarding teammates
- Auth0: JSON Web Token Claims - covers standard claims and how to add custom claims correctly
- RFC 7517: JSON Web Key (JWK) - the spec for how public keys are published and consumed, relevant when configuring RS256 verification
- Security Stack Exchange: JWT vs OAuth comparison - a well-threaded discussion that clarifies the common confusion between JWTs as a token format and OAuth as an authorization framework
EvvyTools also has a broader set of developer utilities at evvytools.com if you need other browser-based tools during development workflows.
Wrapping Up
When a 401 shows up, the fastest path to diagnosis is reading the token directly. The payload tells you whether the issue is expiry, audience mismatch, a missing claim, or something the server is enforcing separately. Keep production tokens out of third-party decoders. Use client-side tools or a one-liner in the browser console. And treat short expiry as a default, not an optimization.
Top comments (0)