Skip to main content
The Auth UI Boilerplate uses JSON Web Tokens (JWT) to authenticate requests from the Next.js frontend to your backend API. This enables stateless authentication where your backend can verify user identity without directly accessing the session database.

How JWT Authentication Works

1

User Signs In

User authenticates via Better Auth (email/password or OAuth). A session is created and stored in the database.
2

JWT Generation

When the frontend makes an API request, Better Auth generates a signed JWT containing the user’s identity.
3

Token Injection

The API proxy automatically injects the JWT as a Bearer token in the Authorization header.
4

Backend Validation

Your backend validates the JWT signature using the public key from the JWKS endpoint, then processes the authenticated request.

JWT Plugin Configuration

The JWT plugin is configured in both server and client configurations:

Server Configuration

src/lib/auth.ts
import { betterAuth } from "better-auth";
import { jwt } from "better-auth/plugins";

export const auth = betterAuth({
  // ... other config
  plugins: [jwt()],
});
This enables JWT token generation and creates the JWKS endpoint for public key distribution.

Client Configuration

src/lib/auth-client.ts
import { jwtClient } from "better-auth/client/plugins"
import { createAuthClient } from "better-auth/react"

export const authClient = createAuthClient({
  baseURL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL || "http://localhost:3000",
  plugins: [jwtClient()],
})
The client plugin provides methods to retrieve JWTs when needed.

Token Generation

JWT tokens are generated server-side using Better Auth’s getToken API:
src/app/api/[...path]/route.ts
const result = await auth.api.getToken({
  headers: await headers(),
})

if (!result?.token) {
  return NextResponse.json(
    { error: 'Authentication required' },
    { status: 401 }
  )
}

const token = result.token
This method:
  • Validates the current session from the request cookies
  • Generates a signed JWT containing user claims
  • Returns the token for use in backend requests
Tokens are generated on-demand for each backend request, not stored long-term. This ensures tokens are always fresh and reduces security risks.

JWT Structure

A typical JWT generated by Better Auth contains:
{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "key-id-123"
}

Payload

{
  "sub": "user-id-123",
  "email": "[email protected]",
  "iat": 1234567890,
  "exp": 1234571490
}

Signature

The signature is created using the private key stored in the database, ensuring the token cannot be forged.

JWKS Endpoint

Better Auth automatically provides a JSON Web Key Set (JWKS) endpoint at:
https://your-app.com/api/auth/jwks
This endpoint returns the public keys needed to verify JWT signatures:
{
  "keys": [
    {
      "kty": "RSA",
      "kid": "key-id-123",
      "use": "sig",
      "alg": "RS256",
      "n": "...",
      "e": "AQAB"
    }
  ]
}
Your backend should fetch and cache these public keys to verify JWT signatures.

Backend Token Validation

Here’s how your backend API should validate JWT tokens:
from fastapi import HTTPException, Security
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt
import requests
from functools import lru_cache

security = HTTPBearer()

@lru_cache(maxsize=1)
def get_jwks():
    """Fetch and cache the public keys from JWKS endpoint"""
    response = requests.get("https://your-app.com/api/auth/jwks")
    return response.json()

async def verify_token(credentials: HTTPAuthorizationCredentials = Security(security)):
    token = credentials.credentials
    jwks = get_jwks()
    
    try:
        # Decode header to get key ID
        unverified_header = jwt.get_unverified_header(token)
        kid = unverified_header.get("kid")
        
        # Find matching public key
        key = next((k for k in jwks["keys"] if k["kid"] == kid), None)
        if not key:
            raise HTTPException(status_code=401, detail="Invalid token")
        
        # Verify signature and decode payload
        payload = jwt.decode(
            token,
            key,
            algorithms=["RS256"],
            options={"verify_exp": True}
        )
        
        return payload
    except jwt.ExpiredSignatureError:
        raise HTTPException(status_code=401, detail="Token expired")
    except jwt.InvalidTokenError:
        raise HTTPException(status_code=401, detail="Invalid token")

Token Lifecycle

1

Token Generation

Generated server-side when the API proxy needs to make a backend request.
2

Token Validation

Backend verifies the signature using public keys from JWKS.
3

Token Expiration

Tokens typically expire after a short duration (e.g., 15 minutes) for security.
4

Automatic Refresh

The proxy generates a fresh token for each request, so expiration is handled automatically.

Security Best Practices

Short Expiration

Keep JWT expiration times short (15-30 minutes) to minimize risk if a token is compromised.

JWKS Caching

Cache JWKS responses on your backend to reduce latency and external requests.

HTTPS Only

Always use HTTPS to prevent tokens from being intercepted in transit.

Validate Claims

Verify all claims (exp, iat, sub) in addition to the signature.
Never store JWTs in localStorage or sessionStorage on the client side. The API proxy handles token generation server-side, keeping tokens secure.

Key Storage

JWT signing keys are stored in the jwks table in your database:
  • Private Key: Used to sign tokens (never exposed publicly)
  • Public Key: Distributed via JWKS endpoint for verification
  • Key Rotation: Better Auth can rotate keys for enhanced security
See the Database Schema documentation for more details.

Troubleshooting

Common causes:
  • Session expired or invalid
  • Backend cannot reach JWKS endpoint
  • Clock skew between frontend and backend
  • Token signature verification failed
Check your backend logs for specific JWT validation errors.
If tokens expire too quickly:
  • Ensure system clocks are synchronized (use NTP)
  • Check if token expiration time is appropriate for your use case
  • Verify the API proxy is generating fresh tokens for each request
If your backend cannot fetch the JWKS:
  • Verify the JWKS URL is accessible from your backend
  • Check firewall rules and network connectivity
  • Ensure HTTPS certificates are valid
  • Implement retry logic with exponential backoff

Next Steps

API Proxy

Learn how the API proxy automatically injects JWT tokens

Database Schema

Understand how JWT keys are stored in the database

Build docs developers (and LLMs) love