mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
## What kind of change does this PR introduce? Feature + docs. Stacked on #48161 (logo contract / [DEPR-604](https://linear.app/supabase/issue/DEPR-604/define-connect-logo-asset-and-variant-contract)). ## What is the current behavior? After #48161, curated logos only resolve from allowlisted `redirect_uri` hosts. A requester can still present a trusted partner **name** (e.g. Claude) while redirecting to an unrelated remote host; the UI shows Supabase alone but does not call out the mismatch. ## What is the new behavior? - Shows a caution admonition when the requester name looks like a trusted partner (Claude, Cursor, ChatGPT/OpenAI, Perplexity) but `redirect_uri` is a **remote** host outside that partner's allowlist. - Skips localhost / loopback redirects for the caution (common for local MCP clients); those still get curated logos when the name matches a trusted partner. - Highlights the footer redirect URL in warning colour when the caution is shown. - Documents the behaviour in the Connect interstitials pattern. ### To test Real MCP clients (Claude, Cursor, etc.) only send users to **production** `/authorize`, so you cannot drive a local or preview Studio build from those tools. Use a Network override instead: 1. Start Studio and sign in (`pnpm dev:studio`, or use the [Vercel preview](https://studio-staging-git-danny-oauth-impersonation-warning-supabase.vercel.app/)). 2. Open `/dashboard/authorize?auth_id=foo` (any `auth_id` is fine; the real response may 404) ([Vercel preview](https://studio-staging-git-danny-oauth-impersonation-warning-supabase.vercel.app/dashboard/authorize?auth_id=foo)). 3. DevTools → **Network** → find `GET …/platform/oauth/authorizations/foo` (or whatever id you used). 4. Right-click → **Override content** (enable Local Overrides / pick a folder if prompted). 5. Paste one of the payloads below (status **200**), save, then reload the authorize page. 6. Keep `expires_at` in the future so the request does not look expired. #### Impersonation caution (trusted name + remote non-allowlisted redirect) Expect: - Supabase alone (no curated Claude mark) - Caution: “Redirect does not match this app name” - Footer redirect URL in warning colour ```json { "name": "Claude", "website": "https://claude.ai", "icon": null, "domain": "claude.ai", "redirect_uri": "https://evil.com/callback", "expires_at": "2099-01-01T00:00:00.000Z", "scopes": ["organizations:read", "projects:read"], "approved_at": null, "registration_type": "dynamic" } ``` | Preview | | --- | | <img width="764" height="958" alt="Authorize Claude Supabase" src="https://github.com/user-attachments/assets/e6eee016-5710-41ba-9925-87511e009e22" /> | #### Localhost MCP: no caution Expect curated Claude + Supabase pair (name match + loopback), **no** caution, normal footer colour. Local MCP clients often use loopback redirects. ```json { "name": "Claude", "website": "https://claude.ai", "icon": null, "domain": "claude.ai", "redirect_uri": "http://127.0.0.1:42813/callback", "expires_at": "2099-01-01T00:00:00.000Z", "scopes": ["organizations:read", "projects:read"], "approved_at": null, "registration_type": "dynamic" } ``` | Preview | | --- | | <img width="764" height="958" alt="Authorize Claude Supabase" src="https://github.com/user-attachments/assets/79f36865-3c8e-43e5-9490-24288efc74aa" /> | #### Legitimate curated partner: no caution Expect curated Cursor + Supabase pair, no admonition, normal footer colour. ```json { "name": "Cursor", "website": "https://cursor.com", "icon": null, "domain": "cursor.com", "redirect_uri": "https://cursor.com/callback", "expires_at": "2099-01-01T00:00:00.000Z", "scopes": ["organizations:read", "projects:read"], "approved_at": null, "registration_type": "dynamic" } ``` | Preview | | --- | | <img width="764" height="958" alt="56164" src="https://github.com/user-attachments/assets/412333a3-a74f-42eb-9f63-d56b6a26bf91" /> | #### Unrelated name + remote redirect: no caution Expect Supabase alone (no icon), no admonition. ```json { "name": "Acme Tools", "website": "https://evil.com", "icon": null, "domain": "evil.com", "redirect_uri": "https://evil.com/callback", "expires_at": "2099-01-01T00:00:00.000Z", "scopes": ["organizations:read", "projects:read"], "approved_at": null, "registration_type": "dynamic" } ``` | Preview | | --- | | <img width="764" height="958" alt="Authorize Acme Tools Supabase" src="https://github.com/user-attachments/assets/dab24817-5c26-4aa1-a447-796c4af5868b" /> | <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit - **New Features** - Added an OAuth caution when a requester name matches a known partner but uses an unapproved remote redirect host. - Improved trusted partner logo selection for localhost/loopback redirects while preserving safe fallbacks for untrusted redirects. - **Documentation** - Updated Connect interstitial guidance for redirect mismatches and localhost/loopback behavior. - **Tests** - Expanded coverage for caution visibility, messaging, localhost logo pairing, and trusted redirect scenarios. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
269 lines
9.0 KiB
Plaintext
269 lines
9.0 KiB
Plaintext
---
|
||
title: Connect Interstitials
|
||
description: Shared layout guidance for focused authorisation, invite, marketplace, CLI, and credit redemption flows.
|
||
---
|
||
|
||
Connect interstitials are focused, single-card flows that sit outside the main
|
||
Studio shell. Use the shared `InterstitialLayout` family instead of building
|
||
bespoke centered cards, logos, account rows, or organisation selectors.
|
||
|
||
<ComponentPreview
|
||
name="connect-interstitial-demo"
|
||
description="Centered 400px card with partner branding, account row, and a single primary action"
|
||
align="start"
|
||
className="p-0"
|
||
padded={false}
|
||
peekCode
|
||
wide
|
||
/>
|
||
|
||
## Use this pattern for
|
||
|
||
This pattern fits short-lived connect flows: partner authorisation and consent
|
||
(OAuth, MCP, Stripe Projects), organisation invites, marketplace and billing
|
||
connections (AWS Marketplace, Vercel install, credit redemption), and CLI or
|
||
device-code sign-in. Use the same shell for their loading, error, success, and
|
||
wrong-account states.
|
||
|
||
Do not use it for normal authenticated Studio pages. Those should use the
|
||
standard [page layout](./layout) patterns.
|
||
|
||
## Source of truth
|
||
|
||
```tsx
|
||
import { OrganizationSelector } from '@/components/interfaces/Connect/OrganizationSelector'
|
||
import {
|
||
InterstitialAccountRow,
|
||
InterstitialLayout,
|
||
LogoBox,
|
||
LogoPair,
|
||
PartnerLogo,
|
||
SupabaseLogo,
|
||
} from '@/components/layouts/InterstitialLayout'
|
||
```
|
||
|
||
## Basic shape
|
||
|
||
Use `InterstitialLayout` for the outer card, then put route-specific content in
|
||
`px-6 pb-6`. Widen the card only when the flow embeds a real tool, such as
|
||
project linking.
|
||
|
||
```tsx
|
||
<InterstitialLayout
|
||
logo={
|
||
<LogoPair
|
||
left={<PartnerLogo src={`${BASE_PATH}/img/icons/stripe-icon.svg`} alt="Stripe" />}
|
||
right={<SupabaseLogo />}
|
||
/>
|
||
}
|
||
title="Authorize Stripe Projects"
|
||
description="This will create an organization on your behalf in Supabase"
|
||
>
|
||
<div className="px-6 pb-6">
|
||
<InterstitialAccountRow displayName={displayName} />
|
||
<Button variant="primary" block>
|
||
Continue
|
||
</Button>
|
||
</div>
|
||
</InterstitialLayout>
|
||
```
|
||
|
||
```tsx
|
||
<InterstitialLayout
|
||
logo={<LogoPair left={<VercelLogo />} right={<SupabaseLogo />} />}
|
||
title="Connect Vercel project"
|
||
containerClassName="items-start"
|
||
cardClassName="max-w-[900px]"
|
||
>
|
||
<div className="px-6 pb-6">{projectLinker}</div>
|
||
</InterstitialLayout>
|
||
```
|
||
|
||
## Logos
|
||
|
||
Use `LogoPair` when the user is connecting two known services, and
|
||
`SupabaseLogo` alone for first-party flows or when the requester has no trusted
|
||
mark. `PartnerLogo` fills the 48px box edge-to-edge; `LogoBox` is for custom
|
||
inset marks or logos that need their own background. Store new partner icons in
|
||
`apps/studio/public/img/icons`.
|
||
|
||
### Pairing
|
||
|
||
| Requester logo | Header treatment |
|
||
| -------------------------------------------------------- | ---------------------------------------------------- |
|
||
| Curated partner / MCP client, or a trusted uploaded icon | `LogoPair` with requester left, `SupabaseLogo` right |
|
||
| Unknown, missing, blocked, or failed-to-load icon | `SupabaseLogo` alone. Do not invent an initial tile. |
|
||
|
||
The user is usually arriving from the third-party app. The interstitial should
|
||
confirm they are connecting to Supabase. Put the requester name in the title and
|
||
description; do not manufacture a letter avatar to fill the left side of a pair.
|
||
|
||
**Known services.** When both sides are curated (or otherwise known), pair them.
|
||
Theme-reactive tiles are fine when both marks have matching light/dark
|
||
treatment.
|
||
|
||
<ComponentPreview
|
||
name="connect-interstitial-logo-pair"
|
||
description="LogoPair when the user is connecting two services"
|
||
align="start"
|
||
className="p-0"
|
||
padded={false}
|
||
peekCode
|
||
wide
|
||
/>
|
||
|
||
**No trusted requester mark.** If the icon is missing, blocked, or fails to
|
||
load, show `SupabaseLogo` alone. Do not invent an initial tile to fill the
|
||
pair.
|
||
|
||
<ComponentPreview
|
||
name="connect-interstitial-logo-unknown"
|
||
description="No trusted requester mark: Supabase alone"
|
||
align="start"
|
||
className="p-0"
|
||
padded={false}
|
||
peekCode
|
||
wide
|
||
/>
|
||
|
||
**Uploaded organisation OAuth icons.** Icons published via Studio’s OAuth app
|
||
builder are unclassified bitmaps — we do not know if they were authored for
|
||
light or dark. Treat the pair as light on both Studio themes: fixed light tile
|
||
chrome (`border-black/10 bg-white`, `SupabaseLogo forceLight`) on both sides.
|
||
Do not invent a dark variant for the upload. Toggle the docs theme to dark to
|
||
see the light tiles hold against the Studio chrome.
|
||
|
||
<ComponentPreview
|
||
name="connect-interstitial-logo-uploaded"
|
||
description="Uploaded OAuth app icon: forced-light tiles on both sides"
|
||
align="start"
|
||
className="p-0"
|
||
padded={false}
|
||
peekCode
|
||
wide
|
||
/>
|
||
|
||
### Assets
|
||
|
||
Treat Connect logos as assets, not theme tokens.
|
||
|
||
**Default to light.** Prefer a single static light mark inside `LogoBox`.
|
||
Light assets read fine on both light and dark Studio themes. That is the
|
||
default for Connect tiles.
|
||
|
||
**Keep pairs matched.** In a `LogoPair`, both marks must use the same
|
||
treatment: both light, or both dark. Do not mix a light partner tile with a
|
||
dark-theme-only Supabase treatment, or the reverse. Theme-aware dark variants
|
||
are fine for curated partners that already have them, but then both sides of
|
||
the pair should use the dark set together.
|
||
|
||
**What not to do**
|
||
|
||
- Do not invent light/dark pairs for arbitrary remote OAuth icons.
|
||
- Do not recolour vendor SVGs with theme CSS. Monochrome identity-provider
|
||
masks (for example GitHub on sign-in) stay a separate pattern.
|
||
|
||
**Where logos come from on `/authorize`**
|
||
|
||
- Curated partner logos resolve from allowlisted `redirect_uri` hosts, or from
|
||
a trusted partner name when `redirect_uri` is localhost / loopback (local MCP
|
||
clients). Do not resolve curated logos from self-asserted `name` or `website`
|
||
on a remote host. Those pairs may use theme tiles and dark assets when the
|
||
partner has them.
|
||
- Published organisation OAuth app icons uploaded in Studio remain trusted
|
||
remote images, paired with forced-light tiles on both sides.
|
||
- Everything else falls back to `SupabaseLogo` alone.
|
||
- If the requester name looks like a known partner but `redirect_uri` is a
|
||
remote host outside that partner's allowlist, show a caution admonition.
|
||
Localhost MCP redirects are excluded.
|
||
|
||
## Account row
|
||
|
||
Use `InterstitialAccountRow` for signed-in context. Do not recreate it locally.
|
||
|
||
```tsx
|
||
<InterstitialAccountRow avatarUrl={avatarUrl} displayName={displayName} action={signOutButton} />
|
||
```
|
||
|
||
## Organisation selection
|
||
|
||
Use `OrganizationSelector` when the flow needs an organisation pick. Extend it
|
||
for new states instead of inventing a parallel card style.
|
||
|
||
```tsx
|
||
<OrganizationSelector
|
||
organizations={linkableOrganizations}
|
||
selectedSlug={selectedOrgSlug}
|
||
onSelect={setSelectedOrgSlug}
|
||
createLabel="Create new organization"
|
||
onCreate={() => setShowOrgCreationDialog(true)}
|
||
/>
|
||
```
|
||
|
||
## Actions
|
||
|
||
Prefer one full-width primary action. A full-width text button is fine for a
|
||
secondary action that still belongs in the flow.
|
||
|
||
### Action feedback
|
||
|
||
Match feedback to its scope:
|
||
|
||
- Use `FormMessage` or `FieldError` beside a field when that field needs to
|
||
change.
|
||
- Show a submission or action failure as simple destructive text below the
|
||
actions. Separate it with a subtle divider when needed for composition. Keep
|
||
the current account, selections, and actions visible so the user can retry.
|
||
- Use `Admonition` when the whole interstitial is blocked or has materially
|
||
changed state, such as an invalid link, wrong account, or partially completed
|
||
setup.
|
||
- Use a toast only for non-blocking feedback or a completed action whose
|
||
originating surface is no longer visible. A toast must not be the only
|
||
feedback for a failure the user needs to resolve on the current card.
|
||
|
||
```tsx
|
||
<InterstitialActionError error={actionError} />
|
||
```
|
||
|
||
Clear stale action feedback when the user retries or changes a relevant
|
||
selection. Error copy should say what failed and, when it is not obvious, what
|
||
the user can do next. When passive supporting copy occupies the same footer
|
||
region, replace it with the action error until the error is cleared instead of
|
||
stacking both messages.
|
||
|
||
<ComponentPreview
|
||
name="connect-interstitial-action-error"
|
||
description="Retryable action error shown beside the actions"
|
||
align="start"
|
||
className="p-0"
|
||
padded={false}
|
||
peekCode
|
||
wide
|
||
/>
|
||
|
||
## States
|
||
|
||
Keep loading, invalid, error, and success states inside the same card when the
|
||
route can explain them. Use `ShimmeringLoader` for loading, and `Admonition`
|
||
for warning, error, note, and success copy.
|
||
|
||
<ComponentPreview
|
||
name="connect-interstitial-logo-single"
|
||
description="Wrong-account warning inside the same interstitial card"
|
||
align="start"
|
||
className="p-0"
|
||
padded={false}
|
||
peekCode
|
||
wide
|
||
/>
|
||
|
||
## Copy
|
||
|
||
Use sentence case. Prefer `sign in` over `login`. Titles and primary actions
|
||
should follow `Verb -> Thing`, for example `Authorize Stripe Projects` or
|
||
`Install Vercel`.
|
||
|
||
Keep the layout title static across states and put state-specific copy in the
|
||
body. Header descriptions should stay short and should not end with a full
|
||
stop.
|