Skip to content

UX: OAuth Server consent screen is visually inconsistent with the dashboard #822

@salmon-21

Description

@salmon-21

Summary

The OAuth Server consent screen rendered by generateAuthorizeHtml (src/controllers/oauthServerController.ts) is a self-contained inline-HTML page with its own hardcoded styles, completely separate from the React dashboard's design system. Side by side with /login (Better Auth, dashboard-styled) or any of the Dashboard pages, it looks like a different product. For a screen whose entire purpose is to be a trust signal — "yes, this is the same MCPHub you've been administering, this consent prompt is legitimate" — the visual jump is actively counter-productive and gives a phishing replica a meaningfully easier bar to clear.

This is filed as a separate concern from the consent-screen content additions tracked in #821, because the two are orthogonal: the content fix doesn't depend on visual unification and vice versa.

What the consent screen currently uses

In generateAuthorizeHtml:

  • An inline <style> block with hardcoded colors (#eef5ff container background, #23408f title, #2563eb approve button, etc.).
  • A separate font stack (-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif) declared inline; no relation to the SPA's font setup.
  • No reference to any shared design tokens, no Tailwind utilities, no link to the bundled SPA CSS.
  • A custom button shape, custom spacing, custom border radius — all duplicated rather than reused.

The React dashboard (Tailwind-based) has its own design tokens for the same things. The result is that /login (LoginPage.tsx) renders within the dashboard's visual language but /oauth/authorize does not, even though they're surfaces of the same authentication flow.

Why it matters

  1. Trust signal. The consent screen is the most security-critical surface in the entire product: the moment where a user grants long-lived access to their MCP servers. A user who has spent any time in the dashboard learns its visual language. When the consent screen looks unrelated to that language, two things happen:

    • The screen reads as "tossed-off" or "third-party," reducing the credibility of the approval ask.
    • A phishing replica only has to imitate the inline page, which is a much smaller target than imitating the entire dashboard. The trust gain of "this looks like the dashboard I know" disappears.
  2. Inconsistency with adjacent auth surfaces. Better Auth's /login page already uses the dashboard's design system (it's part of the SPA). The OAuth Server consent flow is the only auth-related surface that drops out of the design system, which makes the inconsistency even more jarring than it would be for a one-off page.

  3. Maintenance drift. Every dashboard theme/branding change will leave the consent screen behind unless someone remembers to also touch this template. The two surfaces will diverge over time absent active intervention.

Implementation directions

Three sketches in increasing-invasiveness order; happy to be told which the project prefers:

  • (α) Same inline-render, dashboard tokens. Pull the dashboard's design tokens (the same color / spacing / font variables Tailwind compiles from) into a small shared CSS string that both generateAuthorizeHtml and the React app reference. Lowest disruption; gets ~80% of the visual parity. The page stays server-rendered, so the auth flow's coupling to the SPA build doesn't change.

  • (β) Same inline-render, link to bundled dashboard CSS. generateAuthorizeHtml emits a <link rel="stylesheet" href="${assetsPath}/dashboard.css"> (or whatever name the build produces) and styles its container with dashboard utility classes. The consent page is still server-rendered, but visually identical to the SPA. Introduces an implicit dependency on the SPA bundle being available at render time, which is already true in any deployment that serves the dashboard.

  • (γ) Move consent into the React SPA. GET /oauth/authorize serves a minimal HTML shell that boots the SPA at a route like /oauth/consent?.... The React page renders the consent UI using existing components and POSTs back to /oauth/authorize. Most invasive but the long-term right answer — same path Better Auth already takes for /login. Has the side benefit of letting the consent UI use the same i18n, accessibility, and component primitives as everything else.

(α) is the obvious "small PR, big visual win" option to land first. (β) is a midpoint if the dashboard's Tailwind output isn't easy to extract as discrete tokens. (γ) probably warrants its own RFC if anyone wants to take it on.

Out of scope (filed / will file separately)

Filing for visibility — realistically can't commit to a PR on this one, but happy to bikeshed any of the three directions if that's useful.


🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions