feat(oauth): name and show the icon of the requesting app on the consent screen (BUNYIP-406) #406

Merged
longjacksonle merged 2 commits from feat/BUNYIP-406-consent-app-identity into main 2026-07-29 01:11:25 +02:00

What

The consent dialog read only "An application is requesting access to your information." It now names - and, when set, shows the icon of - the requesting app, and its copy names the user's Bunyip account.

Root cause of the missing name (confirmed)

The mokosh-apps client (the app users authorize from) was registered first-party, which by design skips the consent screen (BUNYIP-342). Production only showed the generic dialog because it is stale (predates BUNYIP-342's first-party skip + name threading). The code already names third-party clients correctly.

Per the confirmed decision, mokosh-apps now shows a named consent: the migration clears its first_party flag and sets its display name to "Mokosh" (was the internal "mokosh-apps"). Consent only appears for un-consented scopes, so users who already granted are not re-prompted.

Icon

  • New nullable logo_uri column on oauth_clients, loaded on the existing runtime OAuthClient query (FromRow - no .sqlx regen) and threaded through the consent redirect the same way client_name already is.
  • bunyip-web renders an app-identity row: the logo <img> when a safe URL is set, otherwise a name-initial badge, so the card always shows a clean identity. Mokosh ships with no logo (there is no Mokosh logo asset yet), so it shows the "M" badge; a real logo drops in later via the column with no code change.

Copy

"{name} is requesting access to your Bunyip account."

Security

Name and logo URL are rendered by Maud as escaped text/attributes (no markup injection). Additionally only https / same-origin logo URLs are emitted as an <img> - javascript: / data: / plaintext http: degrade to the initial badge and are never requested - matching the page CSP.

Acceptance criteria

  • Root cause identified (stale deploy + first-party skip) and fixed (mokosh-apps -> third-party, named).
  • Consent dialog displays the requesting app's name (after redeploy).
  • Icon shown when available; absence degrades cleanly (initial badge).
  • Copy names the user's Bunyip account.
  • Spoofed name / logo URL cannot inject markup (test); hostile schemes yield no <img>.
  • Real Mokosh -> Bunyip observed run - for post-deploy confirmation.

Verification

  • just check-container green: fmt + clippy -D warnings + all test binaries (124 web tests). New tests: Bunyip-account copy, logo-scheme allowlist, and escaping of a spoofed name/logo.
  • Migration applied against a throwaway Postgres: name -> Mokosh, first_party -> false, logo_uri TEXT/nullable.
  • Headless render of the card attached below.

Note: load_client is a runtime query_as (FromRow), so adding the column needed no .sqlx regeneration.

🤖 Generated with Claude Code

## What The consent dialog read only "An application is requesting access to your information." It now names - and, when set, shows the icon of - the requesting app, and its copy names the user's Bunyip account. ## Root cause of the missing name (confirmed) The mokosh-apps client (the app users authorize from) was registered **first-party**, which by design **skips** the consent screen (BUNYIP-342). Production only showed the generic dialog because it is **stale** (predates BUNYIP-342's first-party skip + name threading). The code already names third-party clients correctly. Per the confirmed decision, mokosh-apps now shows a **named** consent: the migration clears its `first_party` flag and sets its display name to **"Mokosh"** (was the internal "mokosh-apps"). Consent only appears for un-consented scopes, so users who already granted are not re-prompted. ## Icon - New nullable `logo_uri` column on `oauth_clients`, loaded on the existing **runtime** `OAuthClient` query (FromRow - **no `.sqlx` regen**) and threaded through the consent redirect the same way `client_name` already is. - bunyip-web renders an app-identity row: the logo `<img>` when a safe URL is set, otherwise a **name-initial badge**, so the card always shows a clean identity. Mokosh ships with no logo (there is no Mokosh logo asset yet), so it shows the "M" badge; a real logo drops in later via the column with no code change. ## Copy `"{name} is requesting access to your Bunyip account."` ## Security Name and logo URL are rendered by Maud as escaped text/attributes (no markup injection). Additionally only `https` / same-origin logo URLs are emitted as an `<img>` - `javascript:` / `data:` / plaintext `http:` degrade to the initial badge and are never requested - matching the page CSP. ## Acceptance criteria - [x] Root cause identified (stale deploy + first-party skip) and fixed (mokosh-apps -> third-party, named). - [x] Consent dialog displays the requesting app's name (after redeploy). - [x] Icon shown when available; absence degrades cleanly (initial badge). - [x] Copy names the user's Bunyip account. - [x] Spoofed name / logo URL cannot inject markup (test); hostile schemes yield no `<img>`. - [ ] Real Mokosh -> Bunyip observed run - for post-deploy confirmation. ## Verification - `just check-container` green: fmt + clippy `-D warnings` + all test binaries (124 web tests). New tests: Bunyip-account copy, logo-scheme allowlist, and escaping of a spoofed name/logo. - Migration applied against a throwaway Postgres: `name -> Mokosh`, `first_party -> false`, `logo_uri` TEXT/nullable. - Headless render of the card attached below. Note: `load_client` is a runtime `query_as` (FromRow), so adding the column needed no `.sqlx` regeneration. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(oauth): name and show the icon of the requesting app on the consent screen (BUNYIP-406)
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 50s
Check / fmt + clippy + build + tests (pull_request) Successful in 4m18s
b7023eaf85
The consent dialog read only "An application is requesting access to your information", never naming the requester. Two things fixed.

Root cause of the missing name: the mokosh-apps client (the app users authorize from) was registered first-party, which by design SKIPS the consent screen (BUNYIP-342); production only showed the generic dialog because it is stale (predates that + the BUNYIP-342 name threading). Per the confirmed decision, mokosh-apps now shows a NAMED consent - the migration clears its first_party flag and sets its user-facing display name to "Mokosh" (was the internal "mokosh-apps"). Consent only appears for un-consented scopes, so users who already granted are not re-prompted.

Application icon: a nullable logo_uri column on oauth_clients, loaded on the existing runtime OAuthClient query (FromRow, so no .sqlx regen) and threaded through the consent redirect the same way client_name already is. bunyip-web renders it as an app-identity row on the card. When no logo is set (the Mokosh default for now - there is no Mokosh logo asset yet) it degrades to a name-initial badge, so the card always shows a clean identity and the real logo drops in later with no code change.

Copy now names the account: "{name} is requesting access to your Bunyip account."

Security: the name and logo URL are rendered by Maud as escaped text/attributes, so a spoofed value cannot inject markup; additionally only https / same-origin logo URLs are emitted as an <img> (javascript:/data:/http degrade to the initial badge and are never requested), matching the page CSP.

Verified: `just check-container` green (fmt + clippy -D warnings + all test binaries, 124 web tests). New tests cover the Bunyip-account copy, the logo-scheme allowlist, and that a spoofed name/logo is escaped and a hostile scheme yields no <img>. The migration was applied against a throwaway Postgres (name -> Mokosh, first_party -> false, logo_uri TEXT/nullable). The real Mokosh->Bunyip observed run (naming Mokosh in the dialog) is for post-deploy confirmation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L5dcYueNHByRnWJDYoDX1W
feat(oauth): use Mokosh's real brand logo on the consent screen (BUNYIP-406)
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 55s
Check / fmt + clippy + build + tests (pull_request) Successful in 4m15s
Create release / Create release from merged PR (pull_request) Has been skipped
93e968f793
Populate the Mokosh client's logo_uri with the real Mokosh brand mark instead of leaving it to the initial-badge fallback. The logo (mokosh-apps/assets/icon-192.png) is self-hosted by bunyip-web at assets/mokosh-logo.png and referenced same-origin ('/assets/mokosh-logo.png'), so the consent icon always loads, needs no cross-origin/CORS, and satisfies the page CSP ('self') - rather than depending on the Mokosh origin's asset paths or uptime.

The migration's UPDATE now sets logo_uri alongside name/first_party for the mokosh-apps client. Verified against a throwaway Postgres (name=Mokosh, first_party=false, logo_uri=/assets/mokosh-logo.png). No Rust change; the rendering + escaping mechanism landed in the previous commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L5dcYueNHByRnWJDYoDX1W
longjacksonle deleted branch feat/BUNYIP-406-consent-app-identity 2026-07-29 01:11:25 +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!406
No description provided.