This walkthrough takes a clean machine to a working bridge in under five
minutes. It uses sqlite + file-backed master key, which is the right
choice for a single-app self-host. Larger deployments can swap in Postgres
(STORAGE=pg, DATABASE_URL=…) and a hosted secret manager
(MASTER_KEY_PROVIDER=gcpsm or file) without touching application code.
The bootstrap and doctor commands enforce the assumptions described
in the zero-code mode design;
this doc is the operator-facing summary.
- Node 24 LTS, pnpm 11.17.0
- An RSA-2048 private key in PKCS#8 PEM (used to sign id_tokens)
- A Toss sandbox / production mTLS cert + key per app you'll register (issued from the Toss console, ~390-day validity)
git clone https://github.com/apps-in-toss-community/oidc-bridge.git
cd oidc-bridge
pnpm install
pnpm buildThis produces dist/server.mjs (the HTTP server) and dist/index.mjs (the
admin CLI). The rest of this doc invokes the CLI as oidc-bridge —
substitute node dist/index.mjs if you haven't put the built CLI on your
PATH.
oidc-bridge bootstrap \
--db-path ./data/oidc-bridge.sqlite \
--master-key-dir ./data/master-keys \
--email [email protected] \
--workspace default \
--issuer-hint https://oidc-bridge.example.comThe command:
- Creates the SQLite database (running migrations).
- Generates a fresh 32-byte master key at
<master-key-dir>/v1.key(mode 0600). - Inserts the first user, the first workspace, and a root admin API token.
- Prints a one-time block of values you must save right now:
Bootstrap complete.
Save these values now — the API token plaintext will not be shown again.
USER_ID=user_…
WORKSPACE_ID=ws_…
API_TOKEN_ID=tok_…
ADMIN_API_TOKEN=tok_…
MASTER_KEY_PATH=./data/master-keys/v1.key (mode 600)
Add to your bridge .env:
STORAGE=sqlite
SQLITE_PATH=./data/oidc-bridge.sqlite
MASTER_KEY_PROVIDER=file
MASTER_KEY_DIR=./data/master-keys
OIDC_ISSUER=https://oidc-bridge.example.com
OIDC_ACTIVE_KID=k1
OIDC_SIGNING_KEY_K1_PEM="$(cat your-signing-key.pem)"
Pass ADMIN_API_TOKEN as --token when running admin CLI commands:
oidc-bridge app create --api-url https://your-bridge.example.com \
--token <ADMIN_API_TOKEN above> ...
Next: run `oidc-bridge doctor` to verify the install.
bootstrap refuses to run a second time against the same master-key
directory or DB — re-bootstrapping would lose access to existing sealed
tokens. To start over, delete both --db-path and --master-key-dir
first, knowing that any sealed tokens in the wild become unrecoverable.
If you don't have one:
node -e 'import("node:crypto").then(({generateKeyPairSync})=>{const {privateKey}=generateKeyPairSync("rsa",{modulusLength:2048});process.stdout.write(privateKey.export({format:"pem",type:"pkcs8"}).toString())})' \
> ./data/signing-key-k1.pemThe bootstrap output assumed OIDC_ACTIVE_KID=k1. The kid name is
arbitrary — the env-var slot (K1) and the kid string just need to match
(case-insensitive; the env var is uppercased, the kid lowercased).
Paste the block from step 2 into a .env file (or your secret store) and
fill in the signing key:
STORAGE=sqlite
SQLITE_PATH=./data/oidc-bridge.sqlite
MASTER_KEY_PROVIDER=file
MASTER_KEY_DIR=./data/master-keys
OIDC_ISSUER=https://oidc-bridge.example.com
OIDC_ACTIVE_KID=k1
OIDC_SIGNING_KEY_K1_PEM="-----BEGIN PRIVATE KEY-----
…
-----END PRIVATE KEY-----
"The ADMIN_API_TOKEN from bootstrap is the credential for admin CLI commands
(--token). It is not a server env var — the server authenticates admin
requests against the api_tokens DB table (bcrypt). Keep the plaintext in
your secret store for use with the CLI.
oidc-bridge doctorThe command runs five probes and prints a report:
| Probe | Green means |
|---|---|
env |
All required env vars present and well-formed |
db |
The configured DB is reachable and migrated |
master-key |
v1.key is present, ≥32 bytes, mode 600 |
jwks |
The active signing key signs and verifies an RS256 JWT |
toss |
mTLS handshake to Toss reaches /login-me |
yellow is "non-fatal warning" — for example, the toss probe is
yellow if you didn't pass --cert and --key, because it has nothing
to handshake with. doctor exits 0 on green or yellow, 1 on red.
To exercise the Toss leg too:
oidc-bridge doctor \
--cert ./local/sandbox.cert.pem \
--key ./local/sandbox.key.pemYou don't need a valid Toss access token — the probe sends a placeholder
and treats Toss returning a FAIL envelope (e.g. INVALID_TOKEN) as a
green result, because that proves the mTLS handshake worked. Only a
network/TLS failure is red.
--json forces JSON output even on a TTY (handy for CI piping):
oidc-bridge doctor --json | jq '.'node dist/server.mjsBy default this binds to 0.0.0.0:8080. Put a TLS-terminating reverse
proxy (Caddy, nginx, Traefik) in front of it. The bridge does not
terminate TLS itself.
Once the server is running, register a Toss mini-app:
oidc-bridge app create \
--workspace-id ws_<from step 2> \
--app-id-toss <toss-mini-app-id> \
--title my-mini-app \
--cert ./local/sandbox.cert.pem \
--key ./local/sandbox.key.pem \
--allowed-origin https://my-mini-app.example.com \
--api-url https://your-bridge.example.com \
--token tok_<ADMIN_API_TOKEN from step 2>The mTLS PEMs are sealed inside the apps row using a key derived from
v1.key; they never leave the bridge.
Two files contain everything that is hard to reproduce:
<master-key-dir>/v*.key— every key version. Lose this and every sealed mTLS PEM and everyait_*token in flight becomes unrecoverable.<sqlite-path>— every workspace, app, user, api-token, and master- key row.
Back up both. If you only back up the DB, you can't decrypt it.
Key rotation is not yet automated via the CLI. For now treat v1.key as
long-lived. If you ever need to rotate, the manual path is: generate a new
v2.key file in the same directory, then migrate apps.sealing_key_version
rows to reference the new version — there is no one-command shortcut today.
doctorreportsdb: red→SQLITE_PATHpoints at a missing or corrupt file. Re-runbootstrapagainst a fresh path or restore from backup.doctorreportsmaster-key: yellow→ the file's mode is wider than 0600.chmod 600 <master-key-dir>/v1.key.doctorreportsjwks: red→ either the active kid isn't in env, or the PEM isn't a valid PKCS#8 RSA key. Regenerate per step 3.doctorreportstoss: red→ mTLS handshake failed. Verify the cert- key match (
openssl x509 -in cert.pem -modulus | openssl md5andopenssl rsa -in key.pem -modulus | openssl md5should match) and that the cert is registered in the Toss console.
- key match (
For ongoing operations (rotating signing keys, adding confidential
clients, capturing fresh Toss fixtures), see RUNBOOK.md.