§6 review checkpoint (todo §6); ran the architecture + product reviewers on the whole project (weighted to the Hydra OAuth2 surfaces) and addressed their findings — no Critical from either. Fixed tests-first: (HIGH, arch) /oauth2/logout was published to Hydra (hydra.yml urls.logout) and asserted in hydra.test.ts but had no handler — a dead/published contract; added hydra-admin.acceptLogoutRequest (PUT logout/accept via the shared reqUrl(kind…)) + a GET /oauth2/logout branch that accepts the RP-initiated logout_challenge → 303 to Hydra's post-logout redirect (missing→400, stale 4xx→recoverable 400, 5xx→500, byte-identical degrade to the login/consent siblings; GET-accept is safe since the challenge is Hydra-minted+single-use; the first-party POST /logout still owns ending the Kratos session + JWT cookie). (HIGH, arch) added oauth2 to RESERVED_PLUGIN_IDS so a plugins/oauth2/ folder can't silently shadow the provider routes (the route surface the §4 reserved-id fix missed; discovery now refuses it loud). (Product Blocker) the third-party consent screen now names the signed-in account — "Signed in as <email>" (ConsentView.account from whoami) — plus a CSRF-guarded "Not you? Sign out" form, so consent is informed on shared devices. (MEDIUM, arch) consent accept() now projects id_token claims only when the live Kratos session subject === the challenge subject Hydra bound at login, never leaking a mismatched session's email/name into the issued token (guards the auto-accept path too). (Product nits) register-form confidential-vs-public guidance + a client-detail "delete and re-register / secret shown once" note (no-edit friction + lost-secret). New tests across discovery (reserved oauth2), hydra-admin (acceptLogoutRequest contract), oauth-consent (subject-match + account-in-view), app.test (logout 303/400/500 matrix, consent identity+sign-out, client form/detail copy); e2e/oauth-login.spec asserts the consent screen names the account. Stability-reviewer run as a local PR: APPROVE, no Critical/High — addressed its doc/comment follow-ups (README §6 documents the logout handler + consent identity line; a comment notes the GET-accept is Hydra-validated). Deferred (reviewer-scoped): the host internal route-table (arch M1, now a pure dedup once H1/H2 are point-fixed) → §9; the RP-initiated-logout browser/live E2E → §8; redirect-URI scheme allowlist + safeUrl() → §7; full client edit / empty-list state / success-flash → §8/polish. typecheck + 279 units green; full-stack OAuth2 login+consent E2E verified live against real Hydra v26.2.0 then torn down.

This commit is contained in:
2026-06-19 11:47:06 +02:00
parent 1c324b18e3
commit 521c09fa2d
17 changed files with 138 additions and 22 deletions

View File

@@ -32,6 +32,7 @@
</dl>
</section>
<section class="form-card admin-actions" aria-label="Client actions">
<p class="field-hint">To change a client, delete and re-register — this issues a new client ID and secret. The secret is shown only once, at registration.</p>
<a class="btn btn-danger" href="<%= del.action %>"><svg class="ico ico-sm" aria-hidden="true"><use href="#i-trash"/></svg>Delete client</a>
</section>
</div>

View File

@@ -20,6 +20,7 @@
</div>
<%- include("field", form.scopeField) %>
<label class="check"><input type="checkbox" name="public"<% if (form.public) { %> checked<% } %>> Public client (SPA / native app, PKCE — no secret)</label>
<span class="field-hint">Browser and mobile apps can't keep a secret — choose Public. Server-side apps that can store one — leave it Confidential.</span>
<label class="check"><input type="checkbox" name="firstParty"<% if (form.firstParty) { %> checked<% } %>> First-party (auto-grant consent — skip the consent screen)</label>
<div class="form-actions">
<a class="btn" href="<%= form.cancelHref %>">Cancel</a>

View File

@@ -1,9 +1,13 @@
<%#
OAuth2 consent form body (todo §6): the inner of the auth-card form — the CSRF + challenge
hidden inputs, the requested scopes, then Allow / Deny submit buttons (one `decision` field).
Locals: challenge, csrfField, csrfToken, scopes (string[]). Captured by views/oauth-consent.ejs.
OAuth2 consent form body (todo §6): the inner of the auth-card form — the signed-in account, the
CSRF + challenge hidden inputs, the requested scopes, then Allow / Deny submit buttons (one
`decision` field). Locals: account?, challenge, csrfField, csrfToken, scopes (string[]). Captured
by views/oauth-consent.ejs.
-%>
<% const labels = { email: "Your email address", offline_access: "Stay signed in (offline access)", openid: "Verify your identity", profile: "Your basic profile (name)" }; -%>
<% if (locals.account) { -%>
<p class="auth-sub">Signed in as <strong><%= account %></strong></p>
<% } -%>
<input type="hidden" name="<%= csrfField %>" value="<%= csrfToken %>">
<input type="hidden" name="consent_challenge" value="<%= challenge %>">
<% if (scopes.length) { -%>