Skip to main content

    Auth Modes: API-Key Mint vs Local Signing

    Behest supports two ways to produce a short-lived, per-end-user JWT. Both yield the same token format — Kong doesn't know (or care) which path you used. Pick based on latency, scale, and operational preference.


    TL;DR

    API-key mint (default)Local signing
    HowPOST /v1/auth/mint with behest_sk_live_...Sign RS256 JWT on your server with a tenant-scoped private key
    Per-user network hop1 (to Behest)0
    Secret you storeAPI keyPrivate key (PEM)
    Good forMost apps; simple opsHigh QPS backends; edge functions; air-gapped
    RevocationRotate API key → all unexpired tokens still valid until expRotate kid → JWKS drops old key → tokens fail
    Setup time30s (create key in dashboard)5 min (generate key, upload public half)

    Rule of thumb: start with API-key mint. Migrate to local signing only if the mint call shows up in your latency budget, or if you want to mint millions of tokens without talking to Behest.


    Mode 1: API-key mint

    Your backend holds a long-lived API key (behest_sk_live_...). When a user needs to talk to Behest, you call:

    http
    POST https://amber-fox-042.behest.app/v1/auth/mint
    Authorization: Bearer behest_sk_live_xxxxxxxxxxxx
    Content-Type: application/json
     
    { "user_id": "user_123", "ttl": 900, "tier": 2, "session_id": "sess_abc" }

    The mint endpoint lives on your project slug host ({slug}.behest.app), not on a shared api.behest.app domain. The iss claim in the returned JWT is the string "https://api.behest.app" for backward compatibility, but that is an identifier — not a network endpoint you call.

    json
    {
      "jwt": "eyJhbGciOi...",
      "project_id": "663e...",
      "ttl": 900,
      "session_id": "sess_abc"
    }

    The returned JWT carries claims { tid, pid, uid: "user_123", tier: 2, sid: "sess_abc", exp } and is signed by Behest's global key. Kong verifies it by fetching /.well-known/jwks.json.

    Via the v1.5 SDK (recommended over raw HTTP):

    ts
    import { Behest } from "@behest/client-ts";
    const behest = new Behest(); // reads BEHEST_KEY (behest_sk_live_*) + BEHEST_BASE_URL
    const { token, ttl, sessionId, expiresAt } = await behest.auth.mint({
      user_id: "user_123",
      tier: 2,
      ttl: 900,
      session_id: "sess_abc", // optional — SDK generates one if omitted
    });

    When to use

    • You're building a typical web or mobile app.
    • You do not yet have performance pressure on mint.
    • You want the simplest possible ops story.

    Rotation

    • Create a new API key in dashboard → update env var → delete the old key.
    • Already-minted JWTs keep working until their exp. If you need instant invalidation, shorten ttl (default 15 min).

    Never do this

    • ❌ Ship behest_sk_live_* to a browser, mobile app, or untrusted device.
    • ❌ Mint a JWT with user_id the client picks. Derive it from your own auth.

    Mode 2: Local signing

    Your backend holds an RS256 private key scoped to your tenant. You sign JWTs yourself, with the same claims Behest would produce. You upload only the public half; Behest serves it via JWKS so Kong can verify.

    Setup

    1. Dashboard → Project → Signing KeysGenerate. Behest returns a PEM private key + a kid. Copy the private key — it is shown once.
    2. Base64-encode the PEM and prefix it with behest_pk_ (or just set BEHEST_KEY to the raw PEM — the SDK accepts either). The public key is automatically added to the tenant JWKS; Kong picks it up within a few minutes.

    Sign a token (v1.5 SDK auto-detects mode by key prefix)

    TypeScript:

    ts
    import { Behest } from "@behest/client-ts";
     
    // Env:
    //   BEHEST_KEY=behest_pk_<base64-encoded-PEM>
    //   BEHEST_KID=<kid from dashboard>
    //   BEHEST_TENANT_ID=<tid>
    //   BEHEST_PROJECT_ID=<pid>
    //   BEHEST_BASE_URL=https://<slug>.behest.app
    const behest = new Behest();
     
    const { token, ttl, sessionId } = await behest.auth.mint({
      user_id: "user_123",
      tier: 2,
      ttl: 900,
      session_id: "sess_abc", // optional
    });

    No HTTP round-trip — the SDK signs locally via jose. The same code works in Node, Cloudflare Workers, Vercel Edge, and Deno.

    Python:

    python
    from behest import Behest
    # Env as above (BEHEST_KEY starting with behest_pk_).
    behest = Behest()
    result = await behest.auth.mint(user_id="user_123", tier=2, ttl=900)
    token = result.token

    Raw (any language) — sign a standard RS256 JWT with this header/payload:

    json
    // header
    { "alg": "RS256", "typ": "JWT", "kid": "<your kid>" }
    // payload
    { "iss": "https://api.behest.app", "aud": "behest", "tid": "<tenant>", "pid": "<project>",
      "uid": "user_123", "tier": 2, "role": "user", "scp": [],
      "iat": 1734..., "nbf": 1734..., "exp": 1734..., "jti": "<uuid>", "sid": "sess_abc" }

    When to use

    • You mint > 100 tokens/sec and the extra hop hurts.
    • You run at the edge (Cloudflare Workers, Deno Deploy, Lambda@Edge) and want zero cold-start network calls.
    • You have a policy requirement for "no outbound secrets traffic".

    Rotation

    • Generate a new signing key (new kid).
    • Deploy the new key to your backend and switch over.
    • Revoke the old kid in dashboard → JWKS drops it → all unexpired JWTs signed with the old kid fail immediately.
    • This gives you real revocation. API-key mint cannot.

    Revocation matrix

    ScenarioAPI-key mintLocal signing
    Leaked API key / private keyRotate key; old JWTs live until expRotate + revoke kid; old JWTs die instantly
    User logout (single user)JWT expires at ttlJWT expires at ttl
    Global kill (all users)Rotate + set short TTL globallyRevoke kid
    Tier downgradeNext mint reflects new tierNext sign reflects new tier

    For most apps, short ttl (5–15 min) is sufficient — Kong also checks a kill-switch flag on every request (kill_switch:{pid} in Redis) for per-project emergency stops.


    Sessions

    Behest's session memory is keyed by {pid, uid, sid}. There are two ways to pin sid:

    1. Mint-time session_id (recommended, both modes): include session_id in the mint body (API-key mint) or in the signed claims (local signing). Kong injects X-Session-Id from the sid claim — the browser can't override it.
    2. Header-only X-Session-Id (legacy): the client sets the header on each request.

    ⚠️ Known limitation (until PLAN §7.2 ships): Kong does not yet validate that the X-Session-Id header is scoped to the caller's uid. Any authenticated user in the same project who can guess or obtain another user's session id can read that user's ephemeral session context. Mitigations:

    • Use UUIDs, never incrementing or user-derived ids (e.g. crypto.randomUUID(), not checkout_${userId}_${ts}). Unguessable by construction.
    • Prefer mint-time session_id so the header cannot be overridden from the browser.
    • Never base access decisions on X-Session-Id — it is a scoping hint, not an authenticated claim, until server-side validation lands.
    • Threads (X-Thread-Id) are already scoped server-side by uid and are not affected.

    Dual mode in the SDK

    @behest/client-ts v1.5 auto-picks a mode from the BEHEST_KEY prefix:

    ts
    // Mode 1 — API-key mint (BEHEST_KEY=behest_sk_live_...)
    const behest = new Behest({
      key: process.env.BEHEST_KEY,
      baseUrl: "https://amber-fox-042.behest.app",
    });
    await behest.auth.mint({ user_id: "user_123", tier: 2 });
     
    // Mode 2 — Local signing (BEHEST_KEY=behest_pk_<base64-PEM> + BEHEST_KID/TENANT_ID/PROJECT_ID)
    // Same code as Mode 1 — the SDK detects the prefix. No HTTP round-trip on mint.
    const behest = new Behest(); // reads everything from env
    await behest.auth.mint({ user_id: "user_123", tier: 2 });

    There is no browser "bring-your-own-token" mode in the SDK — browsers never construct a Behest instance. Instead, your backend mints a JWT with this SDK, hands it to the browser, and the browser uses the OpenAI SDK directly (new OpenAI({ apiKey: token, baseURL, dangerouslyAllowBrowser: true })). See React + Vite quickstart for the full pattern.

    See the TypeScript SDK and Python SDK docs for the full option surface.


    See also

    Enterprise Token FinOps: Enforce hard budgets and attribute costs per session.

    Learn more