Authentication
Token Verification
Verify HelloJohn JWTs in your backend — zero network calls, JWKS caching, and language-specific examples for Go, Node.js, Python, and more.
HelloJohn issues signed JWTs (EdDSA / Ed25519). Your backend verifies them locally using HelloJohn's public keys — no network call to HelloJohn on every request.
How verification works
1. User sends request with Authorization: Bearer {access_token}
2. Your backend fetches HelloJohn's public key from JWKS endpoint (cached)
3. Your backend verifies the JWT signature, expiry, issuer, and audience locally
4. If valid → extract claims and handle the request
5. If invalid → return 401The JWKS endpoint is:
https://your-instance.hellojohn.dev/.well-known/jwks.jsonCache the JWKS response for at least 1 hour. HelloJohn rotates keys infrequently — fetching JWKS on every request adds unnecessary latency and load.
Verification examples
package middleware
import (
"context"
"net/http"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jwt"
)
var keyCache *jwk.Cache
func init() {
ctx := context.Background()
keyCache = jwk.NewCache(ctx)
keyCache.Register("https://your-instance.hellojohn.dev/.well-known/jwks.json",
jwk.WithMinRefreshInterval(time.Hour),
)
// Warm the cache on startup
keyCache.Refresh(ctx, "https://your-instance.hellojohn.dev/.well-known/jwks.json")
}
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tokenStr := r.Header.Get("Authorization")
if len(tokenStr) < 8 || tokenStr[:7] != "Bearer " {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
tokenStr = tokenStr[7:]
keySet, err := keyCache.Get(r.Context(), "https://your-instance.hellojohn.dev/.well-known/jwks.json")
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
token, err := jwt.Parse([]byte(tokenStr),
jwt.WithKeySet(keySet),
jwt.WithValidate(true),
jwt.WithIssuer("https://your-instance.hellojohn.dev"),
jwt.WithAudience("your-app"),
)
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), "user", token)
next.ServeHTTP(w, r.WithContext(ctx))
})
}import { createRemoteJWKSet, jwtVerify } from 'jose'
const JWKS = createRemoteJWKSet(
new URL('https://your-instance.hellojohn.dev/.well-known/jwks.json'),
{ cacheMaxAge: 3600_000 } // cache for 1 hour
)
export async function verifyToken(token) {
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'https://your-instance.hellojohn.dev',
audience: 'your-app',
})
return payload
}
// Express middleware
export function requireAuth(req, res, next) {
const auth = req.headers.authorization
if (!auth?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'unauthorized' })
}
verifyToken(auth.slice(7))
.then(payload => {
req.user = payload
next()
})
.catch(() => res.status(401).json({ error: 'unauthorized' }))
}from functools import lru_cache
import time
import jwt
import requests
@lru_cache(maxsize=1)
def get_jwks(ttl_hash=None):
"""Cached JWKS fetch — refreshes every hour."""
response = requests.get(
"https://your-instance.hellojohn.dev/.well-known/jwks.json"
)
return response.json()
def round_time(ttl=3600):
return round(time.time() / ttl)
def verify_token(token: str) -> dict:
jwks = get_jwks(ttl_hash=round_time())
public_keys = {}
for key_data in jwks["keys"]:
kid = key_data["kid"]
public_keys[kid] = jwt.algorithms.OKPAlgorithm.from_jwk(key_data)
header = jwt.get_unverified_header(token)
key = public_keys.get(header["kid"])
if not key:
raise ValueError("Unknown key ID")
return jwt.decode(
token,
key,
algorithms=["EdDSA"],
audience="your-app",
issuer="https://your-instance.hellojohn.dev",
)
# FastAPI dependency
from fastapi import Depends, HTTPException, Header
async def get_current_user(authorization: str = Header(...)):
if not authorization.startswith("Bearer "):
raise HTTPException(401)
try:
return verify_token(authorization[7:])
except Exception:
raise HTTPException(401)require 'jwt'
require 'net/http'
require 'json'
module HelloJohn
JWKS_URI = 'https://your-instance.hellojohn.dev/.well-known/jwks.json'
def self.jwks
@jwks_fetched_at ||= 0
if Time.now.to_i - @jwks_fetched_at > 3600
response = Net::HTTP.get(URI(JWKS_URI))
@jwks = JSON.parse(response)
@jwks_fetched_at = Time.now.to_i
end
@jwks
end
def self.verify(token)
JWT.decode(token, nil, true, {
algorithms: ['EdDSA'],
iss: 'https://your-instance.hellojohn.dev',
verify_iss: true,
aud: 'your-app',
verify_aud: true,
jwks: jwks
}).first
end
endimport com.nimbusds.jose.jwk.source.*;
import com.nimbusds.jose.proc.*;
import com.nimbusds.jwt.proc.*;
public class TokenVerifier {
private static final DefaultJWTProcessor<SecurityContext> processor;
static {
JWKSource<SecurityContext> keySource = JWKSourceBuilder
.create(new URL("https://your-instance.hellojohn.dev/.well-known/jwks.json"))
.cache(Duration.ofHours(1), Duration.ofMinutes(5))
.build();
processor = new DefaultJWTProcessor<>();
processor.setJWSKeySelector(
new JWSVerificationKeySelector<>(JWSAlgorithm.EdDSA, keySource)
);
DefaultJWTClaimsVerifier<SecurityContext> claimsVerifier =
new DefaultJWTClaimsVerifier<>(
new JWTClaimsSet.Builder()
.issuer("https://your-instance.hellojohn.dev")
.audience("your-app")
.build(),
Set.of("sub", "iat", "exp")
);
processor.setJWTClaimsSetVerifier(claimsVerifier);
}
public static JWTClaimsSet verify(String token) throws Exception {
return processor.process(token, null);
}
}Claims reference
After verification, the JWT payload contains:
| Claim | Type | Description |
|---|---|---|
sub | string | User ID (usr_01HX...) |
iss | string | HelloJohn instance URL |
aud | string | Your application audience |
iat | number | Issued at (Unix timestamp) |
exp | number | Expires at (Unix timestamp) |
email | string | User email |
email_verified | bool | Email verification status |
tenant_id | string | Active tenant ID |
roles | string[] | Roles in the current tenant |
scopes | string[] | OAuth scopes (if applicable) |
Custom claims added via JWT Claims → are also included.
Using the HelloJohn SDK (simpler)
If you're using an official SDK, token verification is handled for you:
// Go SDK — verify + extract user in one call
user, err := hjClient.Auth.VerifyAccessToken(ctx, tokenString)
if err != nil {
http.Error(w, "unauthorized", 401)
return
}
fmt.Println(user.ID, user.TenantID)The SDK internally fetches and caches JWKS, validates claims, and returns a typed User struct.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
token is expired | Access token TTL passed | Client must refresh using the refresh token |
invalid issuer | iss doesn't match config | Check HELLOJOHN_INSTANCE_URL in your backend config |
invalid audience | aud claim mismatch | Set audience in your JWT config to match what HelloJohn issues |
unknown key ID | JWKS cache stale after key rotation | Force-refresh JWKS cache |
invalid signature | Token tampered or wrong key | Ensure you're using HelloJohn's JWKS, not a hardcoded key |