JWT
Issue signed JWT access tokens and publish a JWKS endpoint for verification.
The JWT plugin manages a rotating signing key in the database, issues short-lived JWT access tokens for the active session, and publishes the public keys at a JWKS route so any service can verify those tokens without calling back to Kernia.
Installation
Add the plugin to your server
Pass jwt() to your Kernia config. Set the issuer to your base URL so the
iss claim is correct.
import os
from kernia import KerniaOptions
from kernia.auth import init
from kernia.plugins.jwt import JwtOptions, jwt
from .db import adapter
auth = init(KerniaOptions(
database=adapter,
secret=os.environ["KERNIA_SECRET"],
base_url=os.environ["KERNIA_BASE_URL"],
plugins=[jwt(JwtOptions(issuer=os.environ["KERNIA_BASE_URL"]))],
))Add the client plugin
import { createAuthClient } from "better-auth/client";
import { jwtClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
baseURL: "/api/auth",
plugins: [jwtClient()],
});Usage
Get a token
The recommended path is authClient.token(), which calls /token with the
active session and returns a fresh JWT:
const { data } = await authClient.token();
const jwt = data?.token;A JWT is also returned in the set-auth-jwt response header whenever the client
calls getSession(), so you can capture it without an extra round trip.
Verify a token
Other services fetch the public keys from the JWKS endpoint and verify the token locally — no shared secret, no call back to Kernia:
GET /api/auth/jwksRotate keys
POST /api/auth/jwks/rotate marks the current key inactive and mints a new one.
The old key stays in the JWKS during the grace period so tokens already in flight
keep verifying. Protect the route with rotate_admin_token or an admin session.
Options
Pass these on JwtOptions:
| Option | Type | Default | Description |
|---|---|---|---|
issuer | str | None | Value for the iss claim. Set to your base URL. |
audience | str | None | Value for the aud claim. |
algorithm | str | "EdDSA" | Key-pair algorithm: EdDSA, ES256, ES384, ES512, RS256, or PS256. |
expiration_time | int | str | 900 | Token lifetime in seconds or a string like "15m", "1h", "7d". |
define_payload | (session) -> dict | None | Customize the JWT payload issued by /token. |
get_subject | (session) -> str | None | Override the sub claim. |
jwks_path | str | "/jwks" | Path the JWKS is served from. |
remote_url | str | None | Publish keys remotely; disables the local /jwks route. |
rotation_interval | int | None | Seconds after which a key triggers rotation. |
grace_period | int | None | Seconds a rotated key stays in the JWKS for in-flight tokens. |
rotate_admin_token | str | None | If set, /jwks/rotate requires this bearer token. |
Schema
Adds the jwk table holding each signing key: id, algorithm, public key,
encrypted private key, active flag, and timestamps.
Private keys are encrypted at rest with your KERNIA_SECRET. Rotating that
secret invalidates stored keys, so plan a rotation window if you ever change it.