JWT Authentication Explained for App Developers
JWT authentication works like this: when a user signs in, the auth server issues a JSON Web Token, a small signed document that says who the user is and when the token expires. The client sends that token with every request, usually as Authorization: Bearer <token>, and any server holding the right key can verify the signature and trust the claims without a database lookup. Short-lived access tokens are paired with longer-lived refresh tokens to keep users signed in.
That is the whole idea. The rest of this article covers the details that matter when you build on it: what is inside a token, how signing works, where to store tokens, how to verify them, and the mistakes that cause real security bugs.
#What a JWT looks like
A JWT is three base64url-encoded parts joined by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI4ZjBjIiwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJleHAiOjE3OTQwMDAwMDB9.3q2...- Header: the algorithm and token type.
- Payload: the claims.
- Signature: computed over the first two parts with a key.
Decoded, the first two parts are plain JSON:
{ "alg": "HS256", "typ": "JWT" }{
"sub": "8f0c7a2e-...",
"role": "authenticated",
"aud": "authenticated",
"email": "[email protected]",
"aal": "aal1",
"iat": 1793996400,
"exp": 1794000000
}Base64url is encoding, not encryption. Anyone who has the token can read the payload. Do not put secrets or sensitive personal data in it.
#Registered claims you should know
The JWT standard, RFC 7519, defines a set of registered claims:
| Claim | Meaning |
|---|---|
iss | Issuer: who created the token |
sub | Subject: usually the user id |
aud | Audience: who the token is for |
exp | Expiry time (seconds since epoch) |
iat | Issued at |
nbf | Not valid before |
jti | Unique token id |
Apps add their own claims: role, e-mail, organization, assurance level.
#Signing: HS256 vs RS256 and friends
The signature proves the token was issued by someone holding the key and that nobody changed it.
- HS256 (HMAC with SHA-256) uses one shared secret for signing and verifying. Simple and fast. Every service that verifies tokens needs the secret, so it must stay on trusted servers.
- RS256 / ES256 (RSA or ECDSA) use a private key to sign and a public key to verify. Verifiers never see the signing key; public keys are often published at a JWKS URL. Better when many independent services verify tokens.
Either is fine when used correctly. The important part is that the verifier enforces the expected algorithm.
#Access tokens and refresh tokens
A JWT cannot be "un-issued". Once signed, it is valid until it expires, even if the user signs out or is banned. That is why access tokens are short-lived.
The usual pattern:
- Access token: a JWT, valid for minutes to an hour. Sent with every API request.
- Refresh token: an opaque, random string stored by the auth server. Valid much longer. Exchanged for a new access token when the old one expires.
When the user signs out, the server revokes the refresh token. The current access token keeps working until it expires, so keep its lifetime short.
Refresh token rotation issues a new refresh token on every refresh and invalidates the old one. If a stolen refresh token is used after the real client has already rotated it, the server can detect the reuse.
On StitchDB, access tokens last one hour by default (configurable from 5 minutes to 7 days), refresh tokens rotate on each use, and you can set a maximum session length, an inactivity timeout and single-session-per-user. The SDK refreshes tokens automatically before they expire. See Sessions.
await stitchdb.auth.signOut() // all sessions of the user
await stitchdb.auth.signOut({ scope: 'local' }) // this device only
await stitchdb.auth.signOut({ scope: 'others' }) // every other device#Where to store tokens
There is no perfect answer, only trade-offs.
| Storage | Risk | Notes |
|---|---|---|
localStorage | Readable by any script on the page (XSS) | Simple; common in SPAs |
| In-memory only | Lost on reload | Combine with a refresh mechanism |
httpOnly cookie | Not readable by scripts; needs CSRF protection | Good for server-rendered apps |
| Mobile secure storage (Keychain, Keystore) | Low | The right default on mobile |
The biggest factor is XSS. If an attacker can run script on your page, they can usually act as the user regardless of where the token sits. Invest in a strict Content Security Policy and avoid injecting untrusted HTML.
For server-rendered frameworks, a PKCE flow keeps tokens out of URLs. StitchDB's hosted sign-in pages return a one-time code that the SDK exchanges for a session, so no tokens appear in the address bar or logs.
#Verifying a token on your server
Every verification must check:
- The signature, with the expected algorithm only.
exp(andnbfif present).audandissmatch what you expect.
With the jose library and an HS256 secret:
import { jwtVerify } from 'jose'
const secret = new TextEncoder().encode(process.env.STITCHDB_JWT_SECRET)
export async function verify(token) {
const { payload } = await jwtVerify(token, secret, {
algorithms: ['HS256'],
audience: 'authenticated',
})
return payload // { sub, role, email, ... }
}On StitchDB, access tokens are signed with HS256 using the project's JWT secret, which you copy from Settings › API keys. Alternatively, call auth.getUser(token) to let the auth server check the token and confirm that the account still exists and is not banned. See Roles and JWT.
Local verification is fast but cannot know about revocation. Server-side getUser is authoritative but costs a request. Many apps verify locally on every request and call the auth server for sensitive actions.
#Using JWT claims in the database
On backends built on PostgREST, the token is passed through to Postgres. The request runs as the role named in the role claim (anon, authenticated), and the claims are available in SQL:
select auth.uid(), auth.jwt() ->> 'email', auth.jwt() -> 'app_metadata' ->> 'provider';That is what makes Row Level Security policies like this work:
create policy "users read own orders" on public.orders
for select to authenticated
using (user_id = auth.uid());Use app_metadata claims for authorization; they are set by the server only. user_metadata is editable by the user and must never decide access. More in Postgres Row Level Security.
StitchDB access tokens include sub, role, aud, email, phone, app_metadata, user_metadata, aal and amr (assurance level and sign-in method), session_id, is_anonymous, exp and iat. With Organizations turned on, they also carry org_id, org_role and orgs.
#Mistakes that cause real bugs
- Accepting any algorithm. Historic JWT libraries accepted
alg: noneor let an attacker switch RS256 to HS256 using the public key as the secret. Always pass an explicitalgorithmslist. - Decoding instead of verifying.
jwt.decode()does not check the signature. Use the verify function. - Ignoring
aud. A token issued for one service should not be accepted by another. - Long-lived access tokens. A 30-day access token is a 30-day credential that cannot be revoked.
- Trusting user-editable claims for roles or permissions.
- Sensitive data in the payload. It is readable by anyone who sees the token.
- Putting the signing secret in a client. With HS256, the secret can mint tokens for any user. On StitchDB, the JWT secret and
service_rolekey belong on servers only. - Not handling clock skew. Allow a few seconds of leeway when checking
expandnbf.
#JWT authentication versus server sessions
Classic server sessions store state on the server and give the client an opaque cookie. They are easy to revoke but need a session store lookup per request.
JWTs move the state into the token. They scale well across services and work naturally with APIs and mobile apps, but revocation is harder. The access-plus-refresh pattern is the usual compromise: stateless for most requests, stateful at refresh time.
#FAQ
#Are JWTs encrypted?
Standard signed JWTs (JWS) are not. They are signed. Use JWE if you need encrypted tokens, or keep sensitive data out of them.
#How long should an access token last?
Minutes to an hour is common. StitchDB defaults to one hour.
#Can I revoke a JWT?
Not directly. Revoke the refresh token and keep access tokens short-lived, or check a deny list on sensitive endpoints.
#Should I build my own JWT auth?
Usually not. Use a well-maintained auth server or platform and focus on verifying tokens correctly.
Get Auth with JWTs, RLS and session controls built in: create a free StitchDB project, or read about passwordless login.