Efren Yevale Varela b0e14c9d2f Initial commit il y a 1 mois
..
README.md b0e14c9d2f Initial commit il y a 1 mois
config-sync.json b0e14c9d2f Initial commit il y a 1 mois

README.md

Demo kit — d-OAuth provisioning runbook

Provisions the demo scope, user, entitlement, and redirect URI against a local d-OAuth authorization server, then registers this workspace's confidential BFF client over Dynamic Client Registration (DCR). No real secrets appear below: passwords travel via --password-file, the IAT is printed once by the issue step and handed to DCR, and nothing secret is committed.

Conventions used throughout: demo scope demo.read, demo user demo@example.com, redirect URI http://127.0.0.1:3000/callback. The binary is d-oauth-server (cargo run --bin d-oauth-server -- <command> from the d-OAuth checkout). Flag names below are taken from d-OAuth docs/OPERATIONS.md (command reference).

0. Start the dependencies, then the AS

# From the d-OAuth checkout: migrate once, then serve.
d-oauth-server migrate
D_OAUTH_ISSUER=https://localhost:8080 d-oauth-server serve

serve runs the TLS public listener on 127.0.0.1:8080 (self-signed dev certificate) plus the plaintext health listener on 127.0.0.1:8081. It never migrates and never provisions. Readiness:

curl -fsS http://127.0.0.1:8081/health/ready

1. Scope create / enable

d-oauth-server scope create demo.read --description "Demo read scope"
d-oauth-server scope enable demo.read

scope create upserts (create-or-update-description) and enable flips the catalog flag. Stale-discovery note: if the AS was already running when the scope was created, /metadata scopes_supported may be stale (in-process cache) until restart. The OAuth flow reads Postgres directly, so either restart the AS after scope creation or do not assert scopes from discovery.

2. User create + grant-scope

printf '%s' '<initial-password>' > /tmp/demo-password.txt
d-oauth-server user create --email demo@example.com --password-file /tmp/demo-password.txt
d-oauth-server user grant-scope --email demo@example.com --scope demo.read
d-oauth-server user scopes --email demo@example.com
rm /tmp/demo-password.txt

user create is a plain INSERT, not an upsert: re-running it against an existing user fails with an already-exists error (the provision setup step treats that as create-or-skip). grant-scope requires the scope to exist in the catalog (step 1 first) and upserts. A created user starts with must_change_password = true — see step 6.

Declarative alternative (idempotent; second run reports no changes):

d-oauth-server config sync --file demo/config-sync.json
d-oauth-server config sync --file demo/config-sync.json

demo/config-sync.json covers scopes, redirect URIs, and entitlements. It CANNOT create clients: the sync schema has no clients section, so the confidential client always comes from step 5 (DCR via provision register).

3. Redirect-URI create

d-oauth-server redirect-uri create http://127.0.0.1:3000/callback

The URI is a global catalog entry. After DCR mints the client id, attach it:

d-oauth-server client attach-redirect-uri --client-id <client-id> --uri http://127.0.0.1:3000/callback

(client_redirect_uris in demo/config-sync.json is left empty because the client id only exists after step 5.)

4. IAT issue (single-use, printed once)

d-oauth-server initial-access-token issue --uses 1 --ttl 600

The raw token prints once; only its keyed hash is stored. It is required when allow_open_registration is false. A burned IAT (even one spent on a failed registration) is never reused — issue a fresh one per attempt. Prefer the automated path instead of copy-paste:

cargo run -p provision -- setup --cli <path-to-d-oauth-server> --env-file <d-OAuth-.env> \
  --email demo@example.com --password-file <path> --iat-out /tmp/demo-iat.txt
cargo run -p provision -- register --issuer https://localhost:8080 --iat-file /tmp/demo-iat.txt
rm /tmp/demo-iat.txt

5. DCR (or provision register)

provision register POSTs /register with token_endpoint_auth_method=client_secret_basic, grant_types=[authorization_code,refresh_token], response_types=[code], the demo redirect URI/scope, and dpop_bound_access_tokens=true, then persists config/client.json (mode 0600).

Catalog-policy remediation: under the default dcr_redirect_policy=catalog, only redirect URIs present in the runtime catalog may register. An invalid_redirect_uri rejection means step 3 was skipped: provision the URI first, or relax the AS to dcr_redirect_policy=catalog_loopback (catalog URIs plus dynamic loopback URIs, RFC 8252) — never open in production.

6. Forced password change + TOTP (one-time, browser or Playwright)

The fresh demo user cannot reach /authorize until both complete (restricted session). The automated path is provision bootstrap + the Playwright spec (todo 37), which sets a new known password, captures the otpauth:// secret from /login/totp/setup, and persists {email, password, totp_secret} to config/e2e.json for reuse. Manual equivalent:

  1. POST /login (email, password, hidden csrf; code empty) → sent to /login/change-password.
  2. POST /login/change-password (new_password of at least 12 characters, confirm, hidden csrf).
  3. GET /login/totp/setup → register the shown secret/otpauth:// URI in an authenticator now (shown once, with one-time recovery codes).
  4. POST /login/totp/confirm (code, hidden csrf) → full session.
  5. POST /consent approves with hidden request_id, hidden csrf, and decision=approve.

7. Run the BFF (+ UI in dev)

# BFF (confidential client): http://127.0.0.1:3001, health at /healthz.
cargo run -p server
curl -fsS http://127.0.0.1:3001/healthz

Dev UI (Trunk owns :3000, proxies /oauth/, /callback, /api/ to the BFF on :3001):

cd crates/web && trunk serve --port 3000

Browser entry point: http://127.0.0.1:3000. Approve with decision=approve at /consent; the flow lands back at /callback on the BFF.

8. Sign out / switch users

Log out in the dashboard clears the BFF session, then navigates the browser to the provider logout (<issuer>/logout). d-OAuth ends its own session and 302s back to the app root (http://127.0.0.1:3000/, the client's registered post-logout return URI), so a different account can sign in on the next run without restarting the authorization server.

The scope demo.read is required. If the account authenticates without it, the dashboard keeps the session and shows a Signed in without the required scope notice; if the AS refuses the scope at authorization time, /callback renders a Scope not granted page. Grant the scope (step 2), then sign in again.