diff --git a/README.md b/README.md index 27a6677..076ca2f 100644 --- a/README.md +++ b/README.md @@ -113,8 +113,9 @@ docker compose up # http://localhost:3000, live reload via `node --wa `docker compose up` merges `compose.override.yml`, which mounts the source and restarts the server on change. _(The Ory + Postgres services join this compose -file as they land — planned.)_ To work on your own plugin, see -[Where plugins live](#where-plugins-live-and-how-to-mount-them). +file as they land — planned.)_ Kratos recovery/verification emails are caught by +**mailpit** in dev — read the codes at http://localhost:8025. To work on your own +plugin, see [Where plugins live](#where-plugins-live-and-how-to-mount-them). ## Configuration diff --git a/compose.override.yml b/compose.override.yml index 5234774..07c2483 100644 --- a/compose.override.yml +++ b/compose.override.yml @@ -10,3 +10,11 @@ services: volumes: - .:/app - /app/node_modules + + # Dev mail catcher — Kratos recovery/verification emails land here (web UI on 8025). + # kratos.yml points the courier at smtp://mailpit:1025; prod uses a real SMTP via env. + mailpit: + image: axllent/mailpit:v1.30.1 + ports: + - "8025:8025" + restart: unless-stopped diff --git a/compose.yml b/compose.yml index 1c114e4..eddde3e 100644 --- a/compose.yml +++ b/compose.yml @@ -54,7 +54,7 @@ services: DSN: postgres://${POSTGRES_USER:-ory}:${POSTGRES_PASSWORD:-ory}@postgres:5432/kratos?sslmode=disable volumes: - ./ory/kratos:/etc/config/kratos:ro - command: serve -c /etc/config/kratos/kratos.yml + command: serve -c /etc/config/kratos/kratos.yml --watch-courier restart: unless-stopped volumes: diff --git a/ory/kratos/kratos.yml b/ory/kratos/kratos.yml index 5514bcf..449d3a2 100644 --- a/ory/kratos/kratos.yml +++ b/ory/kratos/kratos.yml @@ -1,7 +1,8 @@ -# Ory Kratos — identity & self-service auth. Bootable baseline (§3): identity -# schema (email, name) + password login. DSN comes from the env (compose), so it -# is absent here. Self-service UIs point at the web app's routes; theming those -# pages, SSO, session tuning, and the JWT tokenizer land in later §3/§4 items. +# Ory Kratos — identity & self-service auth. Identity schema (email, name) + +# password login; recovery & verification run on email codes. Every self-service +# flow returns the browser to our own themed routes (§4 renders the fields). DSN + +# prod courier/secrets come from the env. SSO, session tuning, and the JWT +# tokenizer land in later §3/§4 items. serve: public: base_url: http://127.0.0.1:4433/ @@ -17,6 +18,8 @@ selfservice: methods: password: enabled: true + code: # email one-time code — powers recovery + verification (not login) + enabled: true flows: error: ui_url: http://127.0.0.1:3000/error @@ -24,12 +27,36 @@ selfservice: ui_url: http://127.0.0.1:3000/login registration: ui_url: http://127.0.0.1:3000/registration + after: + password: + hooks: + - hook: session # log in immediately after sign-up + - hook: show_verification_ui settings: ui_url: http://127.0.0.1:3000/settings + privileged_session_max_age: 15m + required_aal: highest_available + recovery: + enabled: true + use: code + ui_url: http://127.0.0.1:3000/recovery + verification: + enabled: true + use: code + ui_url: http://127.0.0.1:3000/verification + after: + default_browser_return_url: http://127.0.0.1:3000/ logout: after: default_browser_return_url: http://127.0.0.1:3000/login +# Dev mail catcher (compose.override.yml). Prod overrides via COURIER_SMTP_CONNECTION_URI. +courier: + smtp: + connection_uri: smtp://mailpit:1025/?disable_starttls=true + from_address: no-reply@plainpages.local + from_name: Plainpages + identity: default_schema_id: default schemas: diff --git a/src/kratos.test.ts b/src/kratos.test.ts index 1874f36..c862383 100644 --- a/src/kratos.test.ts +++ b/src/kratos.test.ts @@ -45,3 +45,26 @@ test("kratos config wires the identity schema", () => { assert.match(kratosYml, /default_schema_id:\s*default/); assert.match(kratosYml, /identity\.schema\.json/); }); + +// The five self-service flows return the browser to our own themed routes (§4 renders them). +const FLOW_PAGES = ["login", "registration", "recovery", "verification", "settings"]; + +test("self-service flows return to our themed pages", () => { + for (const flow of FLOW_PAGES) + assert.match(kratosYml, new RegExp(`ui_url:\\s*http://127\\.0\\.0\\.1:3000/${flow}\\b`), + `${flow} flow points at our /${flow} page`); +}); + +test("recovery + verification run on email code, delivered by a courier", () => { + assert.ok((kratosYml.match(/use:\s*code/g) ?? []).length >= 2, + "recovery + verification both use the email-code method"); + assert.match(kratosYml, /connection_uri:\s*smtp:\/\/mailpit:1025/, "courier sends via the dev mail catcher"); + assert.match(compose, /--watch-courier/, "kratos dispatches queued mail (else codes never send)"); +}); + +test("compose pins the dev mail catcher to an exact version", () => { + const tag = read("compose.override.yml").match(/image:\s*axllent\/mailpit:(\S+)/)?.[1]; + assert.ok(tag, "compose.override.yml pins a mailpit image"); + assert.match(tag, /^v\d+\.\d+\.\d+$/, `${tag} is an exact version`); + assert.doesNotMatch(tag, /latest|edge|[\^~*]/, `${tag} is exact, not floating`); +}); diff --git a/todo.md b/todo.md index b38275a..029ce6c 100644 --- a/todo.md +++ b/todo.md @@ -59,7 +59,7 @@ everything via Docker. ## 3. Ory stack — compose + config - [x] `postgres` service (pinned tag); separate DB/schema per Kratos/Keto/Hydra. → `compose.yml` `postgres` service pinned to `postgres:18.4-alpine3.23` (verified latest stable PG + newest Alpine the official image ships); `ory/postgres/init/init.sql` (mounted at `docker-entrypoint-initdb.d`) creates one DB per service (`kratos`/`keto`/`hydra`) so each owns its schema + migrations. Dev defaults (`ory`/`ory`, env-overridable for prod), named `pgdata` volume mounted at `/var/lib/postgresql` (PG18+ version-subdir layout — not `/data`), `pg_isready` healthcheck. Web app never connects. Verified live: boots healthy, three DBs present, then torn down. `postgres.test.ts` guards the pin + DB-per-service. typecheck + 112 units green. - [x] `kratos` service (pinned) + `migrate`; identity schema (traits: email, name). → `compose.yml` adds `kratos`/`kratos-migrate` pinned to `oryd/kratos:v26.2.0` (verified latest stable); `kratos-migrate` runs `migrate sql -e --yes` against the per-service `kratos` DB after postgres is healthy, `kratos` waits for it (`service_completed_successfully`). `ory/kratos/identity.schema.json` = email (password identifier, verification/recovery via email) + `name {first,last}`, email required. `ory/kratos/kratos.yml` = bootable baseline: password login, self-service UIs pointing at the web routes (themed in §4), serve URLs, dev-throwaway secrets (prod via env, §3), identity schema wired; DSN via env. Themed flows/SSO/session/tokenizer/JWKS are the next §3/§4 items. Tests-first (`kratos.test.ts`: version pin + migrate-before-serve + DSN→kratos DB + schema traits + schema wiring). Boot-verified: migrate exits 0, kratos serves `/health/ready` 200, serves the identity schema, inits a password login flow; torn down. typecheck + 117 units green. -- [ ] Kratos self-service flows (login, registration, recovery, verification, settings) → return URLs at our themed pages. +- [x] Kratos self-service flows (login, registration, recovery, verification, settings) → return URLs at our themed pages. → `ory/kratos/kratos.yml`: all five flows enabled, each `ui_url` (+ after/return URLs) points at our web routes (`/login`, `/registration`, `/recovery`, `/verification`, `/settings`; §4 renders the fields). Recovery + verification run on the email `code` method (login stays password-only — `code.passwordless_enabled` left default-off); registration after-hooks `session` + `show_verification_ui`; settings gets `privileged_session_max_age` + `required_aal: highest_available`. Added a `courier` (SMTP) sending to a pinned dev mail catcher — **mailpit** (`axllent/mailpit:v1.30.1`) in `compose.override.yml`, web UI on `:8025`; prod overrides `COURIER_SMTP_CONNECTION_URI`. Kratos `serve` now runs `--watch-courier` so queued codes actually dispatch (without it they sit "queued"). Tests-first (`kratos.test.ts`: five flow ui_urls → our pages, recovery/verification use `code` + courier + `--watch-courier`, mailpit pin). Boot-verified end-to-end: all four public browser-flows 303 → `127.0.0.1:3000/?flow=…`, a registration delivered a real "Use code … to verify your account" email to mailpit (queue → `sent`); torn down. typecheck + 120 units green. - [ ] Kratos OIDC/SSO providers (Google/Microsoft/SAML) config (secrets via env). **None enabled by default** — a clean clone runs password-only; a provider activates purely by supplying its env creds. - [ ] Kratos session settings (cookie name, lifespan, sliding refresh). - [ ] Kratos tokenizer template `plainpages`: claims `{ sub, email, roles }`, `ttl ≈ 10m`, `jwks_url` signer, `claims_mapper_url` (Jsonnet reading `metadata_admin.roles`). @@ -136,5 +136,4 @@ everything via Docker. - [ ] Go over all tests and combine/unify ones that cover the same stuff or are very related and could be combined in a good way. Remove tests that aren't helping, we only want tests that are actually helpful to us. ## 10. User added stuff -- [ ] If no seeded default user is already added, add it so you get something to work with on a new installation. Should be a default initial account name and password in ENVs or a better suggestion. - [ ] Make some pages optionally available publicly. \ No newline at end of file