Authentication
Flaghoist has two authenticated paths, each with a pluggable verifier.
- Read path: apps evaluating flags. Guarded by an API key.
- Admin path: creating, changing and deleting flags. Guarded by the admin token, by people’s own accounts once you turn them on, or by JWTs from your identity provider.
This page covers the credentials. For accounts, roles, invites, single sign-on and two-factor codes, see Accounts and team access.
createFlagServer((env) => ({ storage, auth: { read: apiKey(env.READ_API_KEY), admin: bearerToken(env.ADMIN_TOKEN), },}))API key (read path)
Section titled “API key (read path)”read: apiKey(env.READ_API_KEY)Clients send it as x-api-key. The comparison is constant-time. The read path only ever returns
evaluated booleans, never flag configuration, so this key gates drive-by scraping, not secrecy (it
ships in your browser bundle).
Bearer token (admin, zero-config)
Section titled “Bearer token (admin, zero-config)”admin: bearerToken(env.ADMIN_TOKEN)Clients send Authorization: Bearer <token>. Possession of the token is admin authorization: the
simplest self-host default.
Generate a strong one and never a memorable one. Anyone who guesses or steals it has full admin access, and Flaghoist does not rate limit authentication unless you turn it on:
openssl rand -hex 32Changing the token on Cloudflare Workers is one command, and it updates the live secret without a redeploy:
npx wrangler secret put ADMIN_TOKENOn a container host, change the ADMIN_TOKEN environment variable and restart instead.
Rotation is a hard cutover: the moment the secret changes, every client still holding the old token is rejected until you update it. With one holder (you) that costs nothing. Once a team shares the token, turn on accounts: each person gets their own sign-in, role and access tokens, and the admin token is kept for recovery. It has no expiry and no second factor by design; it is the zero-config default, not the end state.
OIDC (admin, with your identity provider)
Section titled “OIDC (admin, with your identity provider)”Validate real JWTs from Cognito, Auth0, Okta, Keycloak, or Entra, with group-based authorization:
import { oidc } from '@flaghoist/server'
admin: oidc({ issuer: env.OIDC_ISSUER, audience: env.OIDC_AUDIENCE, groupsClaim: 'cognito:groups', allowedGroups: ['ADMIN', 'SUPER_ADMIN'], tokenUse: 'id', // Cognito id tokens})This validates the signature against the provider’s JWKS with a pinned algorithm allowlist
(RS256 by default; alg: none is always rejected), checks iss / aud / exp / nbf, and then
requires membership in an allowed group. A valid token that lacks an admin group is rejected with
403.
For the exact config for each provider (issuer format, which claim carries groups, and where to find the values in their console) see OIDC provider setup.
This verifier is for API clients that already hold a JWT from your provider. For people signing in to the dashboard with that provider, use single sign-on instead.
Roles from your own verifier
Section titled “Roles from your own verifier”Every admin route checks a role: viewer, editor, admin or owner. The table of what each can
do is on Accounts and team access. With accounts on, each person’s role comes
from their account. Without them, a verifier can say which role its caller has.
A verifier that returns no role grants owner, the full access every admin verifier had before
roles existed. bearerToken and oidc return no role, so they behave as they always have. A role
that is not one of the four grants nothing.
This gives a second, read-only token to someone who should see the dashboard but not change anything:
const owner = bearerToken(env.ADMIN_TOKEN)const readOnly = bearerToken(env.VIEWER_TOKEN)
admin: async (headers) => { const asOwner = await owner(headers) if (asOwner.ok) return asOwner const asViewer = await readOnly(headers) return asViewer.ok ? { ok: true, identity: 'viewer', role: 'viewer' } : asViewer}A request the caller’s role does not cover is refused with 403 and
{ "error": "This needs the editor role or higher.", "code": "insufficient_role" }. The code
tells it apart from a 403 that rejects the credential itself, and the dashboard uses it to show
the message instead of signing you out.
Security notes
Section titled “Security notes”- A release flag is not an authorization boundary: it controls whether a code path is visible, not whether a user is allowed. Keep authorization in your backend.
- Targeting context is attacker-controlled. Every attribute in an evaluation request is supplied
by the caller and can be anything they choose. If a paywall flag targets
plan == "pro"on the client-suppliedplan, a caller who sendsplan: "pro"is served the feature. Never gate anything that matters on a self-asserted attribute. When a targeting decision must be trustworthy, derive the attribute from a validated session or header and inject it throughtrustedContext, which overrides whatever the client sent. - Set an exact-origin CORS allowlist (
allowedOrigins) for the admin dashboard.
See the threat model for the full picture.