Skip to content

Running a provider

What it takes to put your own provider somewhere real, and what is missing before you should.

One is already running: https://auth.omxus.com, on the Workers and D1 free tiers. To add your site to it, use Registering a site; you do not need your own provider for that.

What a provider is made of

worker/index.js            the Cloudflare Worker: reads env, builds the router
server/router.js           every endpoint; Request in, Response out
server/oidc.js             the protocol: authorize, token, userinfo, JWKS
server/auth.js             challenge → verify → session
server/door.js             which sign-in screen a client gets, and rendering it
server/store.js            storage; in-memory and D1 implementations
server/db-apply.mjs        the list of SQL files, in the order they run
engine/engine.bundle.mjs   the crypto, as one importable module
frontend/dist/             the designed sign-in screen, once built
wrangler.toml              the deployment: vars, D1, assets, email, cron, route

router.js takes plain Request objects and returns plain Response objects. worker/index.js is the only host that is kept working; another runtime (Deno, Bun, node) needs its own short adapter that passes the same options. Untested: the old node demo under docs/oidc-demo/ no longer starts.

What you have to provide

Three secrets. Print them once per deployment:

npm run worker:keys

It prints a wrangler secret put line and a value for each:

Secret What it is If you change or lose it
OIDC_SIGNING_JWK the provider's ES256 private key; signs ID tokens every token in flight stops verifying (at most an hour)
PAIRWISE_SALT server secret behind every pairwise sub every person's sub changes at every site; treat it as permanent
RECOVERY_JWK P-256 key that opens sealed email-recovery secrets lose it and email accounts lose "forgot password"; leak it and a stolen database becomes recoverable

The signing key is the provider's own and has nothing to do with anyone's identity key. Never reuse the recovery key as the signing key. The worker refuses to start without OIDC_SIGNING_JWK.

A D1 database with every migration applied. server/db-apply.mjs lists 20 SQL files, applied in order; npm run worker:db:local applies them to the local database. npm run worker:db:remote applies the same 20 plus server/accounts/avatar-uploads.sql to the remote one. Both scripts name the database omxus-auth-v2: create your own (wrangler d1 create <your-db>) and change the name in package.json and wrangler.toml, with your database_id.

server/oidc-clients/seed-clients.sql is one of those files. It inserts Omxus's own clients, whose redirect addresses are on omxus.com; on your provider they do nothing for anyone else.

Your own values in wrangler.toml. Under [vars], the ones that are yours to set:

Var Set it to
ISSUER your https address. It is baked into every token and every signature people make; changing it later invalidates both
RP_ID the host name passkeys are bound to, usually the issuer's host
FIRST_PARTY_CLIENTS your own sites' client ids (they skip the first-visit "Continue"), or empty
TRUST_CORE, BOOTSTRAP_CITIZENS, CURRENT_QUARTER vouching (see Vouching); the values there are Omxus's own

Also account_id, and the [[routes]] pattern and zone, for your domain.

Email and texts, for codes. Optional. Without them, email and phone sign-up say they are unavailable; everything else runs. See Email and texts.

Optional secrets. ACCOUNTS_OPERATOR_SECRET (and ACCOUNTS_OPERATOR_IPS) turn on the operator routes. SETTLE_SECRET turns on the vouch minting queue; unset, it does not exist.

Registering clients

On auth.omxus.com, sites register themselves: see Registering a site. Your own provider has the same /developers page and POST /oidc/register, so the same steps work against your issuer.

To add one by hand, a row in oidc_clients is a client:

npx wrangler d1 execute <your-db> --remote --command \
  "INSERT INTO oidc_clients (client_id, name, redirect_uris, secret_hash, sector_identifier, door)
   VALUES ('my-site','My Site','[\"https://example.com/callback\"]',NULL,NULL,NULL)"

secret_hash is NULL for a browser client and sha256Hex(secret) (from server/oidc.js) for a server-side one. redirect_uris is JSON, and each entry is matched character for character. door picks the sign-in screen: minimal (the dark card), full (the whole Omxus app) or standard (ordinary accounts; also needs the standard door's own columns, so register those through /oidc/register). NULL reads as minimal.

The designed sign-in screen

cd frontend && npm run build

That writes frontend/dist/, which wrangler.toml serves as static assets ([assets], binding ASSETS). A client on the full door gets that page; without a build, it falls back to the plain page in server/signin-page.js. The minimal door is rendered by the server and needs no build.

Serve it from the provider's own origin. The page's calls to /auth/* need to be same-origin for the session cookie, and it reads the issuer from the provider's own discovery document rather than from the page URL. Assume the page origin and every signature is rejected.

Deploying

From the auth repository, with wrangler.toml holding your values:

npm run worker:db:remote
wrangler secret put OIDC_SIGNING_JWK     # and PAIRWISE_SALT, RECOVERY_JWK
cd frontend && npm run build && cd ..
npm run worker:deploy

worker:deploy builds the /sdk/ files first, then runs wrangler deploy. The hourly cron in wrangler.toml deletes expired codes, challenges, tokens, sessions and throttle rows, and audit rows older than 90 days.

Cloudflare Workers free tier, D1 free tier and static assets, on a domain you already own: nothing per month, with headroom for roughly 30,000 sign-ins a day. To try it on your machine first, see Run it locally.

Before you call it production

Read the security model first. Two things are still missing: a scope-by-scope consent screen and back-channel logout. Built and live: refresh tokens on the standard door, self-service client registration (ten an hour from one address, see Registering a site), and sign-in throttling (429 too_many_requests, wait 15 minutes).