Tokens Reference
Access tokens, refresh tokens, and how to verify JWTs issued by HelloJohn.
Tokens Reference
HelloJohn issues two types of tokens per session: a short-lived access token (JWT) and a long-lived refresh token (opaque).
Access Token
The access token is a JSON Web Token (JWT) signed with EdDSA (Ed25519). It is:
- Short-lived — 1 hour by default (configurable per tenant)
- Stateless — verified locally without a network call
- Non-revocable — remains valid until expiry even after session revocation
Access Token Structure
eyJhbGciOiJFZERTQSIsImtpZCI6ImtleV8wMUhBQkNERUYifQ.
eyJzdWIiOiJ1c3JfMDFIQUJDREVGMTIzNDU2IiwidGVuYW50X2lkIjoidG50...
.signatureHeader:
{
"alg": "EdDSA",
"kid": "key_01HABCDEF",
"typ": "JWT"
}Payload (Claims):
{
"sub": "usr_01HABCDEF123456",
"session_id": "ses_01HABCDEF999888",
"tenant_id": "tnt_01HABCDEF654321",
"org_id": "org_01HABCDEF777666",
"role": "member",
"mfa_verified": true,
"email": "alice@example.com",
"iat": 1705315800,
"exp": 1705319400,
"iss": "https://api.hellojohn.dev",
"aud": "tnt_01HABCDEF654321"
}Standard Claims
| Claim | Type | Description |
|---|---|---|
sub | string | Subject — the user's ID |
iat | number | Issued at (Unix timestamp) |
exp | number | Expiry (Unix timestamp) |
iss | string | Issuer — https://api.hellojohn.dev |
aud | string | Audience — your tenant ID |
HelloJohn Custom Claims
| Claim | Type | Description |
|---|---|---|
session_id | string | The session this token belongs to |
tenant_id | string | Tenant scope |
org_id | string | Active organization (if any) |
role | string | User's role in the active org or system |
mfa_verified | boolean | Whether MFA was completed this session |
email | string | User's email address |
Custom Claims
Add your own claims via the Hooks system:
{
"sub": "usr_01HABCDEF123456",
"plan": "pro",
"team_id": "team_abc",
"...": "..."
}Refresh Token
The refresh token is an opaque token stored server-side by HelloJohn. It:
- Is long-lived — 30 days by default (configurable)
- Is stateful — can be revoked immediately
- Should be stored in an HttpOnly, Secure cookie
Refresh Token Rotation
HelloJohn rotates refresh tokens on every use. Each refresh call:
- Validates the old refresh token
- Issues a new access token
- Issues a new refresh token
- Invalidates the old refresh token
This limits the window of exposure if a refresh token is leaked.
Verifying Access Tokens
Option 1: Local JWT Verification (Recommended)
Fetch the JWKS once and verify locally. Fast, no API call per request:
import * as jose from "jose";
const JWKS = jose.createRemoteJWKSet(
new URL("https://api.hellojohn.dev/.well-known/jwks.json")
);
export async function verifyAccessToken(token: string) {
const { payload } = await jose.jwtVerify(token, JWKS, {
issuer: "https://api.hellojohn.dev",
audience: process.env.HELLOJOHN_TENANT_ID,
});
return payload as {
sub: string;
session_id: string;
tenant_id: string;
org_id: string;
role: string;
mfa_verified: boolean;
exp: number;
};
}Next.js middleware example:
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
import { verifyAccessToken } from "./lib/auth";
export async function middleware(req: NextRequest) {
const token = req.headers.get("authorization")?.replace("Bearer ", "");
if (!token) return NextResponse.json({ error: "unauthorized" }, { status: 401 });
try {
const payload = await verifyAccessToken(token);
const res = NextResponse.next();
res.headers.set("x-user-id", payload.sub);
return res;
} catch {
return NextResponse.json({ error: "invalid_token" }, { status: 401 });
}
}Option 2: API Verification
Slower but simpler — call POST /v1/sessions/verify:
const res = await fetch("https://api.hellojohn.dev/v1/sessions/verify", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.HELLOJOHN_SECRET_KEY}`,
"X-Tenant-ID": process.env.HELLOJOHN_TENANT_ID,
"Content-Type": "application/json",
},
body: JSON.stringify({ token }),
});
const { valid, user_id, mfa_verified } = await res.json();JWKS Endpoint
GET https://api.hellojohn.dev/.well-known/jwks.jsonReturns the tenant's current public keys. Keys are cached by HelloJohn's CDN. Rotate keys in the dashboard; old keys remain valid for 24 hours during rotation.
Response:
{
"keys": [
{
"kty": "OKP",
"crv": "Ed25519",
"use": "sig",
"kid": "key_01HABCDEF",
"x": "base64url-encoded-public-key"
}
]
}Token Storage Best Practices
| Token | Recommended Storage | Avoid |
|---|---|---|
| Access token | In-memory (JS variable) | localStorage, cookies |
| Refresh token | HttpOnly, Secure cookie | localStorage, in-memory |
Why in-memory for access tokens?
localStorage is accessible to any JavaScript on the page (XSS risk). Keep the short-lived access token in memory; it's refreshed automatically.
Why HttpOnly cookie for refresh tokens?
HttpOnly cookies cannot be read by JavaScript, protecting against XSS. Use Secure and SameSite=Lax attributes.