← Guides

Developer Guide

JWT for Authentication

Issue a JWT at login, store it in an httpOnly cookie, keep exp short, and inspect HS256 locally. Covers localStorage XSS, refresh tokens, and time claims.

jwt authenticationjson web token authjwt loginhttponly cookie jwths256 versus rs256

Login is a credential check. What you hand back afterward is a session. A JWT is one way to represent that session: three Base64url parts (header, payload, signature) that a server can verify without a round trip to a session table. The payload is readable by anyone who holds the token, so it is a passport, not a lockbox.

This page is about that login path — issuing the token, where the browser stores it, how exp / iat / nbf behave, and why /tools/jwt-debugger verifies HS256 only. API gateways, scopes, and Authorization: Bearer belong in the authorization guide.

Login issues a JWT

The user posts a username and password (or finishes OAuth). On success the auth service builds a payload with sub (stable user id), iat, exp, and maybe email or amr. It signs with a server-side secret (HS256) or a private key (RS256 / ES256) and returns the compact token.

Put identifiers in the payload if the UI needs them. Put permissions that change hourly in the database, or accept that a stolen token keeps those claims until exp. Format a draft in /tools/json-formatter before you sign it.

httpOnly cookies versus localStorage

localStorage is trivial to read from JavaScript. Any XSS that runs on your origin can fetch the token and ship it off-site. An httpOnly Secure SameSite cookie is not visible to scripts. Pair cookies with CSRF defenses. Mobile and CLI clients typically send Authorization: Bearer. Do not screenshot a production token in any debugger, including a local one.

Short exp and a refresh token

A 15-minute exp limits the window if a token leaks. A longer-lived refresh token, stored more carefully and rotated on use, mints a new access JWT. Revoking access then means rejecting the refresh token in a store you do control.

exp, iat, and nbf are NumericDate values: Unix seconds, not milliseconds. Date.now() in JavaScript is milliseconds — divide by 1000. /tools/jwt-debugger shows those three claims as Unix, UTC, and local time, with expired and not-yet-valid badges after the page mounts. Convert a raw second value on /tools/timestamp (1700000000 is 2023-11-14 22:13:20 UTC).

HS256 versus RS256

HS256 is HMAC-SHA256 with a shared secret. Every service that verifies the token needs that secret, which makes leakage loud. RS256 signs with a private key; verifiers only need the public key, which is what you want when many APIs check tokens your auth service issues.

/tools/jwt-debugger decodes every JWT locally and verifies HS256 when you type a secret in the page. Other algorithms are displayed, not verified. A green HS256 check is not a statement about RS256 production tokens. The secret is not uploaded; it still lives in that tab and in your clipboard history.

Inspect header, payload, and time claims

Paste a token into /tools/jwt-debugger. The header should declare alg and usually typ: JWT. Reject tokens whose alg is none or a surprise. The payload should match what you think you issued — sub, not an email as the only identifier if emails can change.

If exp looks like 13 digits, someone stored milliseconds in a field that RFC 7519 defines as seconds. That token is either already “expired” in some libraries or will live for decades in others. Fix the issuer; do not patch it in the client.

Common pitfalls

Putting the JWT in localStorage and calling it done is the classic XSS footgun. Putting role: admin in a token that lasts a week is the classic stale-privilege footgun. Putting a production HMAC secret into an online debugger is the classic leak — this site stays in the browser, but a screen share does not.

Also: trusting the payload without verifying the signature, and comparing exp to Date.now() without dividing by 1000.

  • /tools/jwt-debugger verifies HS256 only.
  • exp / iat / nbf are Unix seconds.
  • Never paste production secrets into a shared screen or a third-party form.

Worked examples

Typical HS256 header

Input
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Output
{
  "alg": "HS256",
  "typ": "JWT"
}

This is the standard Base64url header. Decode any token on /tools/jwt-debugger to see yours.

Login payload with time claims

{
  "sub": "user_42",
  "email": "dev@example.com",
  "iat": 1700000000,
  "nbf": 1700000000,
  "exp": 1700003600
}

iat and nbf are 2023-11-14 22:13:20 UTC. exp is one hour later (1700003600). Confirm the conversion on /tools/timestamp.

Frequently asked questions

Is a JWT more secure than a session cookie?

Not by default. Security depends on storage, HTTPS, exp, and how you revoke. A JWT in localStorage is usually worse than an httpOnly session cookie.

Why does the debugger refuse to verify my RS256 token?

/tools/jwt-debugger only verifies HS256 with a local secret. It will still decode the header and payload of an RS256 token.

Where should I put the token in a browser app?

Prefer an httpOnly Secure cookie for web sessions. Use Authorization: Bearer from native apps and from server-side callers.

Does thedevtools.in upload my JWT?

No. Decode and HS256 verify run in the browser. Still treat production tokens as secrets — do not screenshot them.

Summary

Issue a short-lived JWT at login, prefer an httpOnly cookie in the browser, and keep a refresh path you can revoke. Inspect exp, iat, and nbf on /tools/jwt-debugger, convert seconds on /tools/timestamp, and never drop a production HMAC secret into a recording of that tab.

Try it yourself

Put what you've learned into practice with our free browser-native tool.

Open Tool →