From d6960c9bada79234cbb1540c9a697cb441031b5f Mon Sep 17 00:00:00 2001 From: lilleman Date: Wed, 17 Jun 2026 10:58:31 +0200 Subject: [PATCH] =?UTF-8?q?Add=20optional=20env-activated=20Kratos=20OIDC/?= =?UTF-8?q?SSO=20providers=20(todo=20=C2=A73);=20off=20by=20default,=20com?= =?UTF-8?q?mitted=20claims=20mapper,=20SAML=20via=20OIDC=20bridge=20note?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 13 ++++++++++++- ory/kratos/kratos.yml | 13 +++++++++++++ ory/kratos/oidc/claims.jsonnet | 16 ++++++++++++++++ src/kratos.test.ts | 14 ++++++++++++++ todo.md | 2 +- 5 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 ory/kratos/oidc/claims.jsonnet diff --git a/README.md b/README.md index 076ca2f..cf0d8c2 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,17 @@ auto-merged by `docker compose up`) turns them back off for live editing. | `JWKS_URL` | Kratos tokenizer JWKS | verifies the session JWT (§4) | | `COOKIE_SECRET` / `CSRF_SECRET` | dev throwaways | enforced by `REQUIRE_SECURE_SECRETS` | +### Social sign-in (SSO) + +Off by default — a clean clone is password-only. Kratos activates a provider purely +from the environment (no code, no rebuild): set `SELFSERVICE_METHODS_OIDC_ENABLED=true` +and `SELFSERVICE_METHODS_OIDC_CONFIG_PROVIDERS` to a JSON array of providers (`google`, +`microsoft`, …), each carrying its `client_id`/`client_secret` and referencing the +committed claims mapper `ory/kratos/oidc/claims.jsonnet`. No creds ⇒ no provider ⇒ no +SSO button (§4 derives the buttons from this list). Open-source Kratos has **no native +SAML** — front it with an OIDC bridge (Ory Polis) and register that bridge as a generic +OIDC provider the same way. + ## Type check & tests ```bash @@ -444,7 +455,7 @@ src/menu-config.ts loadMenuConfig()/defineMenu(): read config/menu.ts (central views/ Core EJS templates (index = the app-shell People dashboard, 403/404/500, partials/ incl. app shell, nav tree, filter bar, data table, pagination, form field, auth card, menu/popover, theme switch, icon sprite) public/ Static assets under /public/ (css/styles.css + auth.css, favicon, robots.txt) config/menu.ts Central menu override + branding (optional; defaults apply if absent) -ory/ Ory service config (kratos/: identity schema + kratos.yml) + storage init (postgres/init/init.sql: one DB per service) +ory/ Ory service config (kratos/: identity schema, kratos.yml, oidc/ SSO claims mapper) + storage init (postgres/init/init.sql: one DB per service) plugins/ Drop-in plugin folders (scanned at /app/plugins; bind-mount or bake in) (planned) docs/ Reference docs (plugin-contract.md — the authoritative plugin API) e2e/ Playwright visual + functional E2E (Dockerfile.e2e + compose.e2e.yml run it) diff --git a/ory/kratos/kratos.yml b/ory/kratos/kratos.yml index 449d3a2..b419238 100644 --- a/ory/kratos/kratos.yml +++ b/ory/kratos/kratos.yml @@ -20,6 +20,19 @@ selfservice: enabled: true code: # email one-time code — powers recovery + verification (not login) enabled: true + # Social sign-in (Google, Microsoft, or SAML via an OIDC bridge like Ory Polis — + # OSS Kratos has no native SAML). OFF by default → a clean clone is password-only. + # Activate WITHOUT code changes by supplying env (the whole-array form is the only + # env-settable one Kratos offers); providers reference the committed claims mapper, + # and §4 derives the buttons from this list: + # SELFSERVICE_METHODS_OIDC_ENABLED=true + # SELFSERVICE_METHODS_OIDC_CONFIG_PROVIDERS=[{"id":"google","provider":"google", + # "client_id":"…","client_secret":"…","scope":["openid","email","profile"], + # "mapper_url":"file:///etc/config/kratos/oidc/claims.jsonnet"}] + oidc: + enabled: false + config: + providers: [] flows: error: ui_url: http://127.0.0.1:3000/error diff --git a/ory/kratos/oidc/claims.jsonnet b/ory/kratos/oidc/claims.jsonnet new file mode 100644 index 0000000..d1c60cf --- /dev/null +++ b/ory/kratos/oidc/claims.jsonnet @@ -0,0 +1,16 @@ +// OIDC claims → identity traits mapper (Kratos exposes the provider's claims as +// `claims`). Shared by every social provider (Google, Microsoft, OIDC/SAML bridges): +// they all expose email + given_name/family_name. Email is required by the schema. +local claims = std.extVar('claims'); + +{ + identity: { + traits: { + email: claims.email, + name: { + first: if std.objectHas(claims, 'given_name') then claims.given_name else '', + last: if std.objectHas(claims, 'family_name') then claims.family_name else '', + }, + }, + }, +} diff --git a/src/kratos.test.ts b/src/kratos.test.ts index c862383..4639a85 100644 --- a/src/kratos.test.ts +++ b/src/kratos.test.ts @@ -62,6 +62,20 @@ test("recovery + verification run on email code, delivered by a courier", () => assert.match(compose, /--watch-courier/, "kratos dispatches queued mail (else codes never send)"); }); +test("social sign-in is off by default — a clean clone stays password-only", () => { + // The oidc method ships present-but-disabled with no providers; operators activate it + // purely via env (SELFSERVICE_METHODS_OIDC_*) — no code change, no baked-in creds. + assert.match(kratosYml, /oidc:\s*\n\s*enabled:\s*false/, "oidc method is disabled by default"); + assert.match(kratosYml, /providers:\s*\[\]/, "no providers baked in"); +}); + +test("the committed OIDC claims mapper maps email + name", () => { + const mapper = read("ory/kratos/oidc/claims.jsonnet"); + assert.match(mapper, /email:\s*claims\.email/, "provider email → email trait"); + assert.match(mapper, /given_name/, "given name → name.first"); + assert.match(mapper, /family_name/, "family name → name.last"); +}); + 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"); diff --git a/todo.md b/todo.md index 029ce6c..142d4a3 100644 --- a/todo.md +++ b/todo.md @@ -60,7 +60,7 @@ everything via Docker. - [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. - [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. +- [x] 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. → `ory/kratos/kratos.yml` adds the `oidc` method present-but-disabled with an empty `providers: []` (clean clone = password-only, boots clean). Activation is pure env, no code/rebuild: `SELFSERVICE_METHODS_OIDC_ENABLED=true` + `SELFSERVICE_METHODS_OIDC_CONFIG_PROVIDERS=[…]` (the whole-array override is the only env-settable form Kratos offers — nested-field env vars aren't supported). Providers (`google`/`microsoft`/OIDC bridges) carry their `client_id`/`client_secret` and reference the committed shared claims mapper `ory/kratos/oidc/claims.jsonnet` (provider claims → `email` + `name{first,last}`). **SAML isn't in OSS Kratos** (Enterprise/Network/Polis only) — documented: front it with an OIDC bridge (Ory Polis) and register that bridge as a generic OIDC provider. README **Social sign-in (SSO)** section documents activation; §4 will derive the buttons from the live provider list. Tests-first (`kratos.test.ts`: oidc disabled + empty by default, mapper maps email/name). Boot-verified both halves: clean stack → login flow has only `default`+`password` groups; a one-off kratos with the SSO env → login flow gains an `oidc` group + a `google` button, no boot errors; torn down. typecheck + 122 units green. - [ ] 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`). - [ ] Generate + mount the JWT signing JWKS; document key rotation.