Authentication
Enable gate.auth so sessions and API keys fill fx.auth — then protect Flows with policy gates.
Authentication is configured as oke({ gate: { auth } }). That bag issues /auth/* Flows
(unless http: false), fills fx.auth, and leaves permission to policy gates — there is no
gate.auth handle for .gate(...).
For developers shipping signed-in APIs on okengine — turn on auth, plug a method, attach policies.
The one rule
Turn on gate.auth for identity. Attach gate.policy / gate.scope (or .public()) for
permission. Boot fails if an HTTP trigger has neither.
Smallest Example
Enable auth on the app
import { oke } from "okengine";
import { username } from "okengine/plugins";
export const app = oke({
name: "notes",
env: "dev",
gate: {
auth: {
// secret required in prod; minted in dev when omitted
// basePath defaults to "/auth"
},
},
}).plug(username());Declare a signed-in policy and attach it
import { gate } from "okengine";
export const member = gate.policy("member", {
description: "Signed-in user",
check: ({ auth }) => !!auth.verified,
});import { on, flow, http } from "okengine";
import { member } from "@/core/gate";
export const get = on(
http.get().gate(member),
flow({
do: async (_, fx) => ({ userId: fx.auth.userId }),
}),
);Sign in and call
# Method routes live under basePath (default /auth) — see your plugged method docs
curl -X GET http://localhost:6530/profile \
-H "accept: application/json" \
-H "authorization: Bearer …"Authenticated callers reach do with fx.auth.userId and fx.auth.scopes set. Anonymous
callers fail the policy → typed Unauthorized.
Progressive Patterns
From Bearer-only identity to cookies, API keys, and method plugins:
Default transport is Authorization: Bearer <access>. The pipeline verifies the token into
fx.auth before gate evaluation:
export const app = oke({
name: "notes",
env: "dev",
gate: { auth: {} },
});In production, set gate.auth.secret (or OKE_AUTH_SECRET). Omitting it in prod throws:
gate.auth: secret is required in production (set gate.auth.secret or OKE_AUTH_SECRET).
Forged, expired, or revoked access tokens map to typed Unauthorized — they never become a
principal.
Options
| Option | Type | Default | Meaning |
|---|---|---|---|
secret | string | minted in non-prod | HMAC for access tokens; required in prod |
basePath | string | "/auth" | HTTP prefix for auth Flows |
http | boolean | true | false skips /auth/* bindings (secret + tables only) |
audience | string | "oke-app" | Access-token audience claim |
emailAndPassword.enabled | boolean | false | Credential method knobs |
emailAndPassword.requireEmailVerification | boolean | false | Block sign-in until verified |
session.accessTtlMs | number | 14m | Access token lifetime |
session.refreshTtlMs | number | 30d | Refresh token lifetime |
session.freshAgeMs | number | 24h | Max age for "fresh" step-up policies |
session.idleTtlMs | number | off | Idle timeout from last activity |
session.absoluteTtlMs | number | off | Absolute lifetime from creation |
session.singleSessionPerUser | boolean | false | One live family per user |
cookies | bag | off | HttpOnly cookie mirror |
secondaryStorage | bag | off | Hot-path KV cache (prefix default "auth:") |
tenant | true | bag | off | Multi-tenancy — see Tenancy |
What fx.auth carries
| Field | Meaning |
|---|---|
userId | Principal id, or null when anonymous |
scopes | ReadonlySet<string> used by gate.scope (may include tenant-role union) |
sessionScopes | Session / JWT scopes before tenant-role union |
verified | Session / credential passed verification |
apiKeyId | Present when the principal is an API key |
Inside do, read identity from fx.auth — not fx.user. World access stays on fx.
Sessions & Cookies
Detailed section
Defaults match a short-lived access token plus a long-lived refresh family. Override only what your product needs.
export const app = oke({
name: "notes",
env: "prod",
gate: {
auth: {
secret: process.env.OKE_AUTH_SECRET!,
session: {
accessTtlMs: 14 * 60 * 1000,
refreshTtlMs: 30 * 24 * 60 * 60 * 1000,
freshAgeMs: 24 * 60 * 60 * 1000,
// idleTtlMs / absoluteTtlMs / singleSessionPerUser when needed
},
cookies: {
enabled: true,
prefix: "oke",
sameSite: "lax",
secure: true,
path: "/",
},
},
},
});| Cookie option | Default | Meaning |
|---|---|---|
enabled | false | Opt-in HttpOnly mirror |
prefix | "oke" | Cookie name prefix |
secure | true | HTTPS-only |
sameSite | "lax" | "strict" | "lax" | "none" |
path | "/" | Cookie path |
crossSubdomain | false | Share across subdomains |
domain | — | Explicit cookie domain |
Freshness: policies that require a recent sign-in should compare session age against
session.freshAgeMs (default 24h). Step-up plugins (e.g. two-factor)
build on the same window.
API Keys
| Field | Type | Meaning |
|---|---|---|
name | string | Label for Console / list |
scopes | string[] | Cannot exceed the creator’s session scopes |
expiresIn | duration string | Optional ("90d", "1h", …) |
ipAllowlist | string[] | Optional source IP allowlist |
rateLimit | { max, per } | null | Optional per-key throttle |
Return shape: { key, secret } — the secret is shown once at create / rotate.
Key management and tenant admin refuse API-key principals:
{
"data": null,
"error": {
"code": "Forbidden",
"message": "You are not allowed to perform this action.",
"data": { "gate": "auth:api-keys", "reason": "session_only" }
}
}Call those methods from a user session. Machine keys authenticate into Flows; they do not mint more keys.
Public routes
Health checks and login endpoints must declare open posture explicitly:
http.get().public();
// equivalent: http.get().gate(gate.public)Auth method Flows under basePath register their own posture; your app routes still need
.gate(...) or .public().
Set gate.auth.http: false when you want tables + Bearer verify without materializing
/auth/* HTTP bindings (embedding / Console-style hosts).
Troubleshooting
Cause: gate.auth: secret is required in production (set gate.auth.secret or OKE_AUTH_SECRET).
Set an explicit secret before shipping — never rely on the minted dev secret in prod.
Token missing, expired, wrong audience, or cookies enabled without sending credentials. Check
Authorization: Bearer, audience, and cookie SameSite / CORS.
Those methods refuse API-key principals (error.data.reason: "session_only"). Call them from a
user session, not a machine key.
Keys are owned by the creator. A different session cannot revoke or rotate another user’s key
(reason: "not_owner").
Enabling gate.auth does not auto-gate your routes. Attach member (or .public()) on every
HTTP trigger — see Boot Posture.
Learn more
- Username plugin — email-free sign-up on
gate.auth - Authorization — scopes and ABAC policies
- RLS — row policies from stamped identity
- Tenancy —
fx.tenant.id - HTTP —
.gate/.publicon triggers