Tutorial: JWT Authentication
JSON Web Tokens (JWTs) are the standard for API authentication. This tutorial shows you how to validate JWTs in Stoma - using a simple shared secret (HMAC) or public keys (RSA via JWKS).
Two Approaches
Section titled “Two Approaches”| Approach | Use when |
|---|---|
| HMAC (shared secret) | You control both token signing and validation (e.g., internal services) |
| JWKS (RSA keys) | Tokens are signed by an external identity provider (Auth0, Supabase, Firebase, etc.) |
We’ll cover both.
Part 1: HMAC Authentication (Shared Secret)
Section titled “Part 1: HMAC Authentication (Shared Secret)”This is the simplest approach. You have a secret that both signs and validates tokens.
Step 1: Create the Gateway
Section titled “Step 1: Create the Gateway”// JWT authentication using an HMAC shared secret.
// Use this approach when you control both token signing and validation
// (e.g., internal services sharing a secret).
// Demo API: https://stoma.opensource.homegrower.club/demo-api
import { createGateway, jwtAuth, cors, requestLog } from "@homegrower-club/stoma";
const gateway = createGateway({
name: "secure-api",
basePath: "/api",
policies: [requestLog(), cors()],
routes: [
{
path: "/users/*",
pipeline: {
policies: [
jwtAuth({
// HMAC shared secret — in production, read from env:
// secret: process.env.JWT_SECRET
secret: "my-shared-secret-key",
headerName: "Authorization",
tokenPrefix: "Bearer",
// Forward JWT claims as upstream headers
forwardClaims: {
sub: "x-user-id",
email: "x-user-email",
role: "x-user-role",
},
}),
],
upstream: {
type: "url",
target: "https://stoma.opensource.homegrower.club/demo-api",
},
},
},
],
});
export default gateway;
Open in Editor
Step 2: How It Works
Section titled “Step 2: How It Works”When a request comes in:
- Extracts the token from the
Authorization: Bearer <token>header - Verifies the signature using the HMAC secret
- Checks token expiration (
expclaim) - If valid, passes the request to upstream
Step 3: Forward Claims to Upstream
Section titled “Step 3: Forward Claims to Upstream”The example above already includes claim forwarding. The forwardClaims option extracts JWT claims and sets them as upstream headers:
x-user-id: user_123abcx-user-email: alice@example.comx-user-role: adminPart 2: JWKS Authentication (RSA Keys)
Section titled “Part 2: JWKS Authentication (RSA Keys)”When your tokens come from an external provider (Auth0, Supabase, Firebase, Okta), you can’t share a secret - instead, you fetch their public keys from a JWKS endpoint.
Step 1: Find Your JWKS URL
Section titled “Step 1: Find Your JWKS URL”Your identity provider exposes keys at a well-known URL:
| Provider | JWKS URL |
|---|---|
| Auth0 | https://YOUR_DOMAIN/.well-known/jwks.json |
| Supabase | https://YOUR_PROJECT.supabase.co/auth/v1/.well-known/jwks.json |
| Firebase | https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com |
| Okta | https://YOUR_OKTA_DOMAIN/oauth2/default/v1/keys |
Step 2: Configure the Gateway
Section titled “Step 2: Configure the Gateway”// JWT authentication using JWKS (RSA public keys) from an external provider.
// Use this approach when tokens are signed by an identity provider like
// Auth0, Supabase, Firebase, or Okta.
// Demo API: https://stoma.opensource.homegrower.club/demo-api
import { createGateway, jwtAuth, cors, requestLog } from "@homegrower-club/stoma";
const gateway = createGateway({
name: "secure-api",
basePath: "/api",
policies: [requestLog(), cors()],
routes: [
{
path: "/users/*",
pipeline: {
policies: [
jwtAuth({
// Fetch public keys from the provider's JWKS endpoint
jwksUrl: "https://your-auth0-domain.auth0.com/.well-known/jwks.json",
// Validate the issuer claim — prevents tokens from other issuers
issuer: "https://your-auth0-domain.auth0.com/",
// Validate the audience claim — prevents tokens for other APIs
audience: "https://api.yourapp.com",
// Forward claims as upstream headers
forwardClaims: {
sub: "x-user-id",
email: "x-user-email",
},
// Cache JWKS for 5 minutes (default) to reduce network calls
jwksCacheTtlMs: 300000,
}),
],
upstream: {
type: "url",
target: "https://stoma.opensource.homegrower.club/demo-api",
},
},
},
],
});
export default gateway;
Open in Editor
Step 3: Understanding the Options
Section titled “Step 3: Understanding the Options”| Option | Purpose |
|---|---|
jwksUrl | Where to fetch public keys |
issuer | Expected iss claim - prevents tokens from other issuers |
audience | Expected aud claim - prevents tokens for other audiences |
forwardClaims | Extract claims and set as upstream headers |
jwksCacheTtlMs | How long to cache the JWKS (reduces network calls) |
Testing JWT Auth
Section titled “Testing JWT Auth”Generate a Test Token
Section titled “Generate a Test Token”For HMAC (Node.js):
const jwt = require('jsonwebtoken');const token = jwt.sign( { sub: 'user_123', email: 'test@example.com', role: 'admin' }, process.env.JWT_SECRET, { expiresIn: '1h' });console.log(token);Test with cURL
Section titled “Test with cURL”# Without token - should failcurl https://your-gateway.com/api/users# {"error":"unauthorized","message":"Missing bearer token","statusCode":401}
# With valid tokencurl -H "Authorization: Bearer YOUR_JWT_TOKEN" \ https://your-gateway.com/api/users# Success! Request proxied to upstream with x-user-id headerCommon Errors
Section titled “Common Errors”| Error | Cause | Fix |
|---|---|---|
Missing bearer token | No Authorization header | Add header with Bearer <token> |
Invalid signature | Wrong secret/key | Check JWT_SECRET or JWKS URL |
Token expired | exp claim in past | Get a fresh token |
Invalid issuer | iss claim doesn’t match | Check issuer config |
Invalid audience | aud claim doesn’t match | Check audience config |
Advanced: Multiple JWT Sources
Section titled “Advanced: Multiple JWT Sources”Need to accept tokens from multiple providers? Create a custom policy:
import { definePolicy, GatewayError, jwtAuth } from "@homegrower-club/stoma";
const multiAuth = definePolicy({ name: "multi-jwt", priority: 10, // AUTH priority handler: async (c, next) => { const auth0Policy = jwtAuth({ jwksUrl: "https://auth0.auth0.com/.well-known/jwks.json", issuer: "https://auth0.auth0.com/", audience: "https://api.example.com", });
const supabasePolicy = jwtAuth({ jwksUrl: "https://xyz.supabase.co/auth/v1/.well-known/jwks.json", issuer: "https://xyz.supabase.co", audience: "authenticated", });
try { // Try Auth0 first await auth0Policy.handler(c, next); } catch (e) { // If Auth0 fails, try Supabase await supabasePolicy.handler(c, next); } },});What’s Next?
Section titled “What’s Next?”- Auth Policies - all authentication options
- OAuth2 Tutorial - token introspection approach
- RBAC Tutorial - role-based access control
- Generate JWTs - mint JWTs for upstream services