HelloJohn / docs
Sessions

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...
.signature

Header:

{
  "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

ClaimTypeDescription
substringSubject — the user's ID
iatnumberIssued at (Unix timestamp)
expnumberExpiry (Unix timestamp)
issstringIssuer — https://api.hellojohn.dev
audstringAudience — your tenant ID

HelloJohn Custom Claims

ClaimTypeDescription
session_idstringThe session this token belongs to
tenant_idstringTenant scope
org_idstringActive organization (if any)
rolestringUser's role in the active org or system
mfa_verifiedbooleanWhether MFA was completed this session
emailstringUser'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:

  1. Validates the old refresh token
  2. Issues a new access token
  3. Issues a new refresh token
  4. Invalidates the old refresh token

This limits the window of exposure if a refresh token is leaked.


Verifying Access Tokens

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.json

Returns 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

TokenRecommended StorageAvoid
Access tokenIn-memory (JS variable)localStorage, cookies
Refresh tokenHttpOnly, Secure cookielocalStorage, 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.


On this page