Wire Kratos session tokenizer template (todo §3); plainpages JWT (sub/email/roles), 10m TTL, Jsonnet claims mapper reading metadata_admin
This commit is contained in:
@@ -455,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)
|
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)
|
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)
|
config/menu.ts Central menu override + branding (optional; defaults apply if absent)
|
||||||
ory/ Ory service config (kratos/: identity schema, kratos.yml, oidc/ SSO claims mapper) + storage init (postgres/init/init.sql: one DB per service)
|
ory/ Ory service config (kratos/: identity schema, kratos.yml, oidc/ SSO claims mapper, tokenizer/ session→JWT 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)
|
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)
|
docs/ Reference docs (plugin-contract.md — the authoritative plugin API)
|
||||||
e2e/ Playwright visual + functional E2E (Dockerfile.e2e + compose.e2e.yml run it)
|
e2e/ Playwright visual + functional E2E (Dockerfile.e2e + compose.e2e.yml run it)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Ory Kratos — identity & self-service auth. Identity schema (email, name) +
|
# Ory Kratos — identity & self-service auth. Identity schema (email, name) +
|
||||||
# password login; recovery & verification run on email codes. Every self-service
|
# 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 +
|
# 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
|
# prod courier/secrets come from the env. The session→JWT tokenizer is wired below;
|
||||||
# tokenizer land in later §3/§4 items.
|
# its JWKS signing key is generated/mounted by the next §3 item.
|
||||||
serve:
|
serve:
|
||||||
public:
|
public:
|
||||||
base_url: http://127.0.0.1:4433/
|
base_url: http://127.0.0.1:4433/
|
||||||
@@ -87,6 +87,18 @@ session:
|
|||||||
name: plainpages_session
|
name: plainpages_session
|
||||||
persistent: true # survive browser restarts
|
persistent: true # survive browser restarts
|
||||||
same_site: Lax
|
same_site: Lax
|
||||||
|
# Session→JWT tokenizer (§4): whoami(tokenize_as: plainpages) mints a short-lived,
|
||||||
|
# locally-verifiable JWT so the hot path never calls Ory. Claims come from the
|
||||||
|
# committed Jsonnet mapper (sub = identity id, email from traits, roles from the
|
||||||
|
# metadata_admin projection); signed with the JWKS the next §3 item generates/mounts.
|
||||||
|
whoami:
|
||||||
|
tokenizer:
|
||||||
|
templates:
|
||||||
|
plainpages:
|
||||||
|
ttl: 10m
|
||||||
|
subject_source: id
|
||||||
|
claims_mapper_url: file:///etc/config/kratos/tokenizer/plainpages.jsonnet
|
||||||
|
jwks_url: file:///etc/config/kratos/tokenizer/jwks.json
|
||||||
|
|
||||||
# Dev throwaways — production supplies real secrets via env (§3). cipher = 32 chars.
|
# Dev throwaways — production supplies real secrets via env (§3). cipher = 32 chars.
|
||||||
secrets:
|
secrets:
|
||||||
|
|||||||
16
ory/kratos/tokenizer/plainpages.jsonnet
Normal file
16
ory/kratos/tokenizer/plainpages.jsonnet
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// Session→JWT claims mapper for the `plainpages` tokenizer (§4). Kratos exposes the
|
||||||
|
// session as `session`; `sub` is set from the identity id (subject_source: id) and
|
||||||
|
// can't be overridden here. roles come from metadata_admin — the per-login projection
|
||||||
|
// of Keto roles the app refreshes at login; absent on a fresh identity ⇒ empty list.
|
||||||
|
local session = std.extVar('session');
|
||||||
|
local meta =
|
||||||
|
if std.objectHas(session.identity, 'metadata_admin') && session.identity.metadata_admin != null
|
||||||
|
then session.identity.metadata_admin
|
||||||
|
else {};
|
||||||
|
|
||||||
|
{
|
||||||
|
claims: {
|
||||||
|
email: session.identity.traits.email,
|
||||||
|
roles: if std.objectHas(meta, 'roles') then meta.roles else [],
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -68,6 +68,23 @@ test("session settings: branded cookie, bounded lifespan, sliding refresh", () =
|
|||||||
assert.match(kratosYml, /earliest_possible_extend:\s*24h/, "sliding-refresh window is set");
|
assert.match(kratosYml, /earliest_possible_extend:\s*24h/, "sliding-refresh window is set");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("session tokenizer template 'plainpages' mints a short-lived signed JWT", () => {
|
||||||
|
// whoami(tokenize_as: plainpages) → a locally-verifiable JWT, so the hot path never
|
||||||
|
// calls Ory (§4). The JWKS signer is generated/mounted by the next §3 item.
|
||||||
|
assert.match(kratosYml, /tokenizer:\s*\n\s*templates:\s*\n\s*plainpages:/, "plainpages template defined");
|
||||||
|
assert.match(kratosYml, /ttl:\s*10m/, "~10m TTL — re-minted on refresh");
|
||||||
|
assert.match(kratosYml, /subject_source:\s*id/, "sub = the Kratos identity id");
|
||||||
|
assert.match(kratosYml, /jwks_url:\s*file:\/\/\/etc\/config\/kratos\/tokenizer\/jwks\.json/, "signs with the mounted JWKS");
|
||||||
|
assert.match(kratosYml, /claims_mapper_url:\s*file:\/\/\/etc\/config\/kratos\/tokenizer\/plainpages\.jsonnet/,
|
||||||
|
"claims via the committed mapper");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the tokenizer claims mapper emits email + roles from the metadata_admin projection", () => {
|
||||||
|
const mapper = read("ory/kratos/tokenizer/plainpages.jsonnet");
|
||||||
|
assert.match(mapper, /email:\s*session\.identity\.traits\.email/, "email ← identity trait");
|
||||||
|
assert.match(mapper, /metadata_admin/, "roles ← metadata_admin (the per-login Keto projection, §4)");
|
||||||
|
});
|
||||||
|
|
||||||
test("social sign-in is off by default — a clean clone stays password-only", () => {
|
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
|
// 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.
|
// purely via env (SELFSERVICE_METHODS_OIDC_*) — no code change, no baked-in creds.
|
||||||
|
|||||||
2
todo.md
2
todo.md
@@ -62,7 +62,7 @@ everything via Docker.
|
|||||||
- [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>?flow=…`, a registration delivered a real "Use code … to verify your account" email to mailpit (queue → `sent`); torn down. typecheck + 120 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>?flow=…`, a registration delivered a real "Use code … to verify your account" email to mailpit (queue → `sent`); torn down. typecheck + 120 units green.
|
||||||
- [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.
|
- [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.
|
||||||
- [x] Kratos session settings (cookie name, lifespan, sliding refresh). → `ory/kratos/kratos.yml` adds a `session` block: branded cookie `name: plainpages_session` (`persistent: true`, `same_site: Lax`), `lifespan: 720h` (30d "stay signed in" backbone the app re-mints the ~10m JWT off, §4), and sliding refresh via `earliest_possible_extend: 24h` (an active session extends back to full lifespan only once within 24h of expiry — no DB write per request). Tests-first (`kratos.test.ts`: cookie name + lifespan + extend window). Boot-verified: kratos serves `/health/ready` 200 with the block; a real browser registration (one-off `--dev` kratos, since Secure cookies don't ride plain http — that's the line-69 split) issued `Set-Cookie: plainpages_session=…; Max-Age=2591999; Expires=…; HttpOnly; SameSite=Lax` — name/persistent/lifespan all as configured; torn down. typecheck + 123 units green.
|
- [x] Kratos session settings (cookie name, lifespan, sliding refresh). → `ory/kratos/kratos.yml` adds a `session` block: branded cookie `name: plainpages_session` (`persistent: true`, `same_site: Lax`), `lifespan: 720h` (30d "stay signed in" backbone the app re-mints the ~10m JWT off, §4), and sliding refresh via `earliest_possible_extend: 24h` (an active session extends back to full lifespan only once within 24h of expiry — no DB write per request). Tests-first (`kratos.test.ts`: cookie name + lifespan + extend window). Boot-verified: kratos serves `/health/ready` 200 with the block; a real browser registration (one-off `--dev` kratos, since Secure cookies don't ride plain http — that's the line-69 split) issued `Set-Cookie: plainpages_session=…; Max-Age=2591999; Expires=…; HttpOnly; SameSite=Lax` — name/persistent/lifespan all as configured; torn down. typecheck + 123 units green.
|
||||||
- [ ] Kratos tokenizer template `plainpages`: claims `{ sub, email, roles }`, `ttl ≈ 10m`, `jwks_url` signer, `claims_mapper_url` (Jsonnet reading `metadata_admin.roles`).
|
- [x] Kratos tokenizer template `plainpages`: claims `{ sub, email, roles }`, `ttl ≈ 10m`, `jwks_url` signer, `claims_mapper_url` (Jsonnet reading `metadata_admin.roles`). → `ory/kratos/kratos.yml` adds `session.whoami.tokenizer.templates.plainpages`: `ttl: 10m`, `subject_source: id` (sub = identity id), `claims_mapper_url`/`jwks_url` pointing at the mounted config dir. `ory/kratos/tokenizer/plainpages.jsonnet` is the claims mapper — `email` from `session.identity.traits.email`, `roles` from the `metadata_admin` projection (§4 refreshes it from Keto at login; absent on a fresh identity ⇒ `[]`, defensive `objectHas`). `sub` is fixed to the identity id by Kratos (`subject_source`), not the mapper. The JWKS signing key referenced by `jwks_url` is generated/mounted by the next §3 item — Kratos loads it lazily at tokenize time, so this boots clean. Tests-first (`kratos.test.ts`: template ttl/subject_source/urls + mapper email/roles-from-metadata_admin). Boot-verified: kratos serves `/admin/health/ready` 200 with the tokenizer wired (config schema accepts the block); torn down. typecheck + 125 units green.
|
||||||
- [ ] Generate + mount the JWT signing JWKS; document key rotation.
|
- [ ] Generate + mount the JWT signing JWKS; document key rotation.
|
||||||
- [ ] `keto` service (pinned) + `migrate`; namespaces in OPL (`role`, `group`, resource permissions).
|
- [ ] `keto` service (pinned) + `migrate`; namespaces in OPL (`role`, `group`, resource permissions).
|
||||||
- [ ] `hydra` service (pinned) + `migrate`; issuer + login/consent URLs → our app.
|
- [ ] `hydra` service (pinned) + `migrate`; issuer + login/consent URLs → our app.
|
||||||
|
|||||||
Reference in New Issue
Block a user