feat(oidc): first-party consent skip + named third-party consent (BUNYIP-342 part 1) #354

Merged
nrupard merged 3 commits from feat/BUNYIP-342-first-party-consent-skip into main 2026-07-10 16:10:56 +02:00
Owner

The backend half of BUNYIP-342. First-party apps (Mokosh and others under Bunyip) no longer hit the consent screen; genuinely third-party clients still get consent, now carrying the client name so the screen can name the requesting app. Mirrors Google not re-prompting consent for its own core apps.

What's here

  • Migration: oauth_clients.first_party BOOLEAN NOT NULL DEFAULT FALSE, and flag mokosh-apps (b0000000-0000-4000-8000-000000000002) as first-party. A new column with a default, so existing offline sqlx query caches are unaffected: the only OAuthClient load is the runtime load_client query_as, which needs no .sqlx regen.
  • OAuthClient gains first_party; load_client selects it.
  • Authorize consent gate (handlers/oidc.rs): with missing scopes, a first-party client has them silently granted (add_scopes_to_grant, so the grant is recorded and revocable via /settings) and falls through to issue the code with no prompt. A third-party client is redirected to /oauth2/consent as before, now with &client_name= added. First-party clients still pass through the BUNYIP-62 tenant gate unchanged.

Verification

just check-container green (fmt + clippy -D warnings + cargo test --workspace --all-targets; bunyip-oidc 266 tests). Migration smoke on postgres:18.2-alpine3.23: all 76 migrations apply clean, first_party column present (default false), mokosh-apps first_party=true, 1/4 seeded clients flagged. The live skip/named behaviour is exercised on the dev-sso OIDC flow (not gate-runnable here).

AC status

  • First-party (Mokosh) skips the consent prompt (skip + auto-grant; behaviour verified on dev-sso).
  • [~] Third-party consent shows the app name: backend passes client_name; the bunyip-web render is part 2.
  • Deny path defined + handled: part 2.
  • [~] Tests: migration + compile/clippy covered here; behavioural flow tests land in part 2.

Part 2 (follow-up)

bunyip-web renders the passed client_name on the consent screen (replacing the generic "An application"), the Deny path is defined and handled, and the first-party-skip / third-party-named-consent flow tests land.

#BUNYIP-342

The backend half of BUNYIP-342. First-party apps (Mokosh and others under Bunyip) no longer hit the consent screen; genuinely third-party clients still get consent, now carrying the client name so the screen can name the requesting app. Mirrors Google not re-prompting consent for its own core apps. ## What's here - **Migration**: `oauth_clients.first_party BOOLEAN NOT NULL DEFAULT FALSE`, and flag mokosh-apps (`b0000000-0000-4000-8000-000000000002`) as first-party. A new column with a default, so existing offline sqlx query caches are unaffected: the only `OAuthClient` load is the runtime `load_client` `query_as`, which needs no `.sqlx` regen. - **`OAuthClient`** gains `first_party`; `load_client` selects it. - **Authorize consent gate** (`handlers/oidc.rs`): with missing scopes, a first-party client has them silently granted (`add_scopes_to_grant`, so the grant is recorded and revocable via /settings) and falls through to issue the code with no prompt. A third-party client is redirected to `/oauth2/consent` as before, now with `&client_name=` added. First-party clients still pass through the BUNYIP-62 tenant gate unchanged. ## Verification `just check-container` green (fmt + clippy `-D warnings` + `cargo test --workspace --all-targets`; bunyip-oidc 266 tests). Migration smoke on `postgres:18.2-alpine3.23`: all 76 migrations apply clean, `first_party` column present (default false), **mokosh-apps first_party=true**, 1/4 seeded clients flagged. The live skip/named behaviour is exercised on the dev-sso OIDC flow (not gate-runnable here). ## AC status - [x] First-party (Mokosh) skips the consent prompt (skip + auto-grant; behaviour verified on dev-sso). - [~] Third-party consent shows the app name: backend passes `client_name`; the bunyip-web render is part 2. - [ ] Deny path defined + handled: part 2. - [~] Tests: migration + compile/clippy covered here; behavioural flow tests land in part 2. ## Part 2 (follow-up) bunyip-web renders the passed `client_name` on the consent screen (replacing the generic "An application"), the Deny path is defined and handled, and the first-party-skip / third-party-named-consent flow tests land. #BUNYIP-342
feat(oidc): skip consent for first-party clients, name the client for third-party (BUNYIP-342 part 1)
Some checks failed
E2E / Playwright against deployment (pull_request) Successful in 7s
Check / fmt + clippy + build + tests (pull_request) Has been cancelled
fa9ee0c3c8
The backend half. Mokosh (and other first-party apps under Bunyip) no longer hit the consent screen; genuinely third-party clients still get consent, now carrying the client name so the screen can name the requesting app. Google does not re-prompt consent for its own core apps; this mirrors that.

- Migration: `oauth_clients.first_party BOOLEAN NOT NULL DEFAULT FALSE`, and flag mokosh-apps (client_id b0000000-0000-4000-8000-000000000002) as first-party. A new column with a default, so the existing offline sqlx query caches are unaffected (the only OAuthClient load is the runtime `load_client` query_as, which needs no `.sqlx` regen).
- `OAuthClient` gains `first_party`; `load_client` selects it.
- Authorize consent gate (handlers/oidc.rs): when scopes are missing, a first-party client has them silently granted (via `add_scopes_to_grant`, so the grant is recorded and stays revocable via /settings) and falls through to issue the code with no prompt. A third-party client is redirected to `/oauth2/consent` as before, now with `&client_name=` added. First-party clients still pass through the BUNYIP-62 tenant gate unchanged.

Verified: just check-container green (fmt + clippy -D warnings + cargo test --workspace --all-targets; bunyip-oidc 266 tests). Migration smoke on postgres:18.2-alpine3.23: all 76 migrations apply clean, `first_party` column present (default false), mokosh-apps first_party=true, 1/4 seeded clients flagged.

Part 2 (follow-up): bunyip-web renders the passed `client_name` on the consent screen (replacing the generic "An application"), and the Deny path is defined and handled; behavioral tests for the first-party skip and third-party named-consent flows land there.

#BUNYIP-342
feat(oidc-web): named third-party consent screen and spec-compliant Deny (BUNYIP-342 part 2)
Some checks failed
E2E / Playwright against deployment (pull_request) Successful in 6s
Check / fmt + clippy + build + tests (pull_request) Has been cancelled
84a2491a52
The frontend half, completing BUNYIP-342. Third-party consent now names the requesting app, and Deny returns the user to the RP with the OIDC access_denied error instead of dropping them on the dashboard.

- bunyip-web consent screen: `ConsentQuery` reads the `client_name` the authorize handler now passes (part 1); `consent_description` renders "<name> is requesting access..." when present and falls back to the generic wording otherwise. Rendered as escaped Maud text, so a spoofed name cannot inject markup.
- Deny path: instead of the dashboard-flash stopgap, bunyip-web bounces the user back through `/oauth2/authorize` with `consent=denied`. bunyip-web never redirects to a redirect_uri itself (that would be an open redirect); the round-trip goes through the authorize handler, which has already validated redirect_uri.
- bunyip-api authorize (`AuthorizeQuery.consent`): after redirect_uri validation, `consent=denied` returns `error=access_denied` (echoing `state`) to the RP per RFC 6749 4.1.2.1, rather than re-rendering the consent gate.

Tests: `consent_description` names the client / falls back on absent or blank; `scope_label` covers known + unknown scopes.

Verified: just check-container green (fmt + clippy -D warnings + cargo test --workspace --all-targets; bunyip-oidc 266, bunyip-web 88 incl. the 2 new). The end-to-end skip / named-consent / deny flows run on the dev-sso OIDC stack (not gate-runnable here).

Completes BUNYIP-342 with part 1 (first-party skip + client_name plumbing).

#BUNYIP-342
fix(oidc-web): reject off-issuer consent continue_url (open redirect) (BUNYIP-342)
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 6s
Check / fmt + clippy + build + tests (pull_request) Successful in 17m57s
Create release / Create release from merged PR (pull_request) Has been skipped
e8cce8eab7
Review follow-up.

- Open-redirect fix: the consent POST redirected to the form's `continue_url` on both Allow (pre-existing) and Deny (added in part 2). `continue_url` is attacker-controllable via a crafted consent GET URL, so a logged-in victim who loads it and submits could be 302'd to an arbitrary host; the Origin CSRF middleware does not cover this (the POST is same-origin, the payload rides in the GET-supplied field). `continue_under_issuer` now requires `continue_url` to be an absolute URL under the OP issuer origin (`cfg.oidc_issuer`) before either path follows it, else it falls back to the dashboard. The trailing-slash boundary check stops a look-alike host (`https://issuer.evil.com/...`) from passing the prefix match. Unit-tested.
- Doc fix: the first-party skip comment claimed the auto-grant "stays revocable via /settings"; corrected to note the skip is unconditional, so revoking a first-party grant does NOT block access (the next authorize re-grants) - intended, since a first-party core app cannot be meaningfully revoked.

Verified: just check-container green (fmt + clippy -D warnings + cargo test --workspace --all-targets; bunyip-web 89 tests incl. the new open-redirect guard).

#BUNYIP-342
nrupard scheduled this pull request to auto merge when all checks succeed 2026-07-10 16:00:27 +02:00
nrupard deleted branch feat/BUNYIP-342-first-party-consent-skip 2026-07-10 16:10:56 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!354
No description provided.