Built-in OAuth2 consent-challenge handler (todo §6); /oauth2/consent grants scopes to a client logging in through us. New src/oauth-consent.ts (pure, sibling of oauth-login.ts): resolveConsentChallenge auto-accepts a first-party client (Hydra metadata.first_party===true) or a Hydra-skipped one, else returns a view to show the themed consent screen; acceptConsent re-reads the challenge so scopes/audience are never client-supplied; rejectConsent → access_denied. The grant carries an OIDC session.id_token with email/name projected from the Kratos identity (whoami traits, omitted when absent). src/hydra-admin.ts gains the consent half (get/accept/reject consent + types; login/consent URL builder folded into one reqUrl(kind,…) + shared put()). Wired in app.ts at GET|POST /oauth2/consent (gated on hydra+kratos): GET shows/auto-accepts (sets the CSRF cookie when fresh), POST is CSRF-guarded (same signed double-submit as /logout) and dispatches allow→accept / else→reject → 303 to Hydra; a stale/consumed challenge (Hydra 4xx) degrades to a recoverable 400, a real outage (5xx) → 500 (mirrors /oauth2/login). views/oauth-consent.ejs + partials/consent-body.ejs reuse the auth-card, listing the requested scopes (friendly labels for the standard OIDC ones) with Allow/Deny submit buttons. Tests-first: hydra-admin consent contracts + oauth-consent skip/first-party/third-party/audience/id_token/refetch/reject matrix + app HTTP integration (auto-accept / screen+CSRF cookie / allow+deny / forged-CSRF→403 / missing→400 / stale→400 / outage→500). Stability-reviewer run as a local PR: APPROVE, no Critical/High. Extended e2e/oauth-login.spec.ts to drive the whole authorization-code flow against real Hydra — login accept → follow login_verifier through Hydra → web's consent screen (third-party e2e-login, scopes listed) → Allow → consent_verifier → client callback with a real code (per-host cookie jars, Hydra resume URLs rebased onto the compose host). typecheck + 262 units + 8 visual + OAuth login+consent E2E green. OAuth2 client registration is the next §6 item.

This commit is contained in:
2026-06-19 10:53:21 +02:00
parent 3c8090e8e3
commit 0900bf49bd
12 changed files with 500 additions and 20 deletions

View File

@@ -241,10 +241,11 @@ docker compose -f compose.yml -f compose.e2e-auth.yml run --build --rm e2e # r
docker compose -f compose.yml -f compose.e2e-auth.yml down -v # tear down after
```
**OAuth2 login challenge** (`oauth-login.spec.ts`) — another app logs in *through* us: it boots the
real stack (incl. Hydra), registers an OAuth2 client, starts an authorization flow, and proves the
§6 `/oauth2/login` handler bounces an unauthenticated user to the themed login and **accepts** the
challenge once a Kratos session exists.
**OAuth2 login + consent** (`oauth-login.spec.ts`) — another app logs in *through* us: it boots the
real stack (incl. Hydra), registers an OAuth2 client, starts an authorization flow, and drives the
§6 handlers end-to-end — `/oauth2/login` bounces an unauthenticated user to the themed login and
**accepts** the challenge once a Kratos session exists; `/oauth2/consent` then shows the consent
screen for the third-party client and **Allow** drives Hydra to issue the authorization code.
```bash
docker compose -f compose.yml -f compose.e2e-oauth.yml run --build --rm e2e # run the suite
@@ -498,7 +499,10 @@ in the menu or first-party pages needs Hydra.
The **login challenge** is wired (`src/oauth-login.ts` at `/oauth2/login`): Hydra hands
the browser here, the app resolves it against the Kratos session and accepts (or bounces
an unauthenticated user to the themed login, returning here once signed in). The **consent
challenge** is next.
challenge** is wired too (`src/oauth-consent.ts` at `/oauth2/consent`): a first-party client
(its Hydra `metadata.first_party: true`) — or one Hydra already skipped — is auto-granted the
requested scopes; any other client gets a themed consent screen whose CSRF-guarded Allow/Deny
accepts or rejects. id_token claims (email, name) come from the Kratos identity.
## Stateless — no application database
@@ -541,8 +545,9 @@ src/jwks.ts JwksProvider — resolve the verify key by kid; createJwksP
src/kratos-public.ts createKratosPublic(): Kratos public-API fetch client — self-service flow init/get/submit, browser logout, whoami, session→JWT tokenize (§4)
src/kratos-admin.ts createKratosAdmin(): Kratos admin-API fetch client — identity CRUD + surgical metadata_public update (login role projection, §4)
src/keto-client.ts createKetoClient(): Keto fetch client — check / list / expand relations (read API) + write / delete tuples (write API) (§4)
src/hydra-admin.ts createHydraAdmin(): Hydra admin-API fetch client — OAuth2 login challenge get/accept/reject (§6)
src/hydra-admin.ts createHydraAdmin(): Hydra admin-API fetch client — OAuth2 login + consent challenge get/accept/reject (§6)
src/oauth-login.ts resolveLoginChallenge(): authenticate a Hydra login challenge via the Kratos session → accept, or bounce to /login (§6)
src/oauth-consent.ts resolveConsentChallenge()/acceptConsent()/rejectConsent(): auto-accept first-party, else show the consent screen → grant scopes (§6)
src/flow-view.ts buildFlowView(): Kratos self-service Flow → themed view model (fields, hidden csrf, buttons, tone-mapped messages) for views/auth.ejs (§4)
src/login.ts completeLogin()/remintSession(): login completion + TTL re-mint — roles from Keto → metadata_public projection → tokenize → session JWT cookie (§4)
src/gen-jwks.ts generateJwks() + CLI: mint the ES256 session-tokenizer signing JWKS (§3); see JWT signing key & rotation
@@ -567,13 +572,13 @@ src/discovery.ts discoverPlugins(): scan plugins/, import + validate each pl
src/router.ts matchRoute()/allowedMethods()/isAuthorized(): map method+path → plugin route, params, permission gate (§2)
src/view-resolver.ts renderPluginView(): render plugins/<id>/views/<view>.ejs; plugin views can include() core partials (§2)
src/menu-config.ts loadMenuConfig()/defineMenu(): read config/menu.ts (central override + branding), validated at boot (§2)
views/ Core EJS templates: index (app-shell dashboard), admin/ (Users/Groups/Roles lists + create/edit/detail + delete-confirm), auth (themed Kratos flows), 403/404/500, partials/ (shell, nav tree, filter bar, data table, pagination, field, auth card, alert, flow + admin bodies, menu/popover, theme switch, icon sprite)
views/ Core EJS templates: index (app-shell dashboard), admin/ (Users/Groups/Roles lists + create/edit/detail + delete-confirm), auth (themed Kratos flows), oauth-consent (OAuth2 consent screen), 403/404/500, partials/ (shell, nav tree, filter bar, data table, pagination, field, auth card, alert, flow + consent + admin bodies, 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, oidc/ SSO claims mapper, tokenizer/ session→JWT claims mapper + dev signing JWKS; keto/: keto.yml + namespaces.keto.ts OPL — role/group/resource; hydra/hydra.yml: OAuth2 issuer + login/consent URLs → /oauth2/*) + 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 E2E: visual.spec (design system, Ory-free) + auth-refresh.spec (token timeout/re-mint) + oauth-login.spec (OAuth2 login challenge), full Ory stack; Dockerfile.e2e + compose.e2e[-auth|-oauth].yml run them
e2e/ Playwright E2E: visual.spec (design system, Ory-free) + auth-refresh.spec (token timeout/re-mint) + oauth-login.spec (OAuth2 login + consent → authorization code), full Ory stack; Dockerfile.e2e + compose.e2e[-auth|-oauth].yml run them
html-css-foundation/ HTML design mockups — the source for the building-block
partials; reference the stylesheets in public/css/.
```