Commit Graph
4 Commits
Author SHA1 Message Date
Gildas Garcia 95ca302439 OAuth Apps Scoped Grants: refactor to ease future implementation (#49958)
## Problem

We're working on OAuth Apps Scoped Grants and want to display an
alternative UI for Authorized apps depending on a feature flag. However,
the `/apps` page currently contains the code of both the _Publishable
apps_ and _Authorized apps_ sections.

## Solution

No functional nor design changes.

- Extract both sections into their own components to ease the
introduction of the alternative UI
- Improve a11y

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added separate views for managing publishable and authorized OAuth
applications.
- Users can publish, update, and delete applications, with convenient
credential-copy controls.
- Authorized applications can be sorted and revoked through a
confirmation dialog.

- **User Experience**
- Added clear loading, empty, error, and permission-denied states for
OAuth application lists.
  - Improved organization settings layout for OAuth app management.
- Added consistent sorting behavior when selecting and toggling table
columns.
- Improved accessibility for credential notifications and dismissal
controls.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-03 16:09:15 +02:00
2bc6144aec fix(studio): guard unguarded requester.name reads on the OAuth authorize and apps pages (#49267)
<!-- ccr-slack-attribution -->
_Requested by **Ali Waseem** · [Slack
thread](https://supabase.slack.com/archives/C063LNYJJKS/p1787146439389169)_

## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Bug fix.

## What is the current behavior?

Opening `/authorize` for an OAuth app whose `name` the platform API
omitted crashed the entire page with `TypeError: Cannot read properties
of undefined (reading 'toLowerCase')`
([SUPABASE-APP-K7E](https://supabase.sentry.io/issues/7679644991/)). The
user got a full-page error instead of a consent screen, and could
neither authorize nor decline.

The same class of crash hit the project-level OAuth apps list
([SUPABASE-APP-JB1](https://supabase.sentry.io/issues/7502074939/)).
Typing in the search box called `.toLowerCase()` on `client_name` for
every app, so one app registered without a name broke search for the
whole list.

The project-claim page crashed the same way, reading the first character
of the name for the fallback avatar.

## What is the new behavior?

The trusted-partner helpers treat a missing name as "no trusted partner
matched" and return `null`. The apps filter treats a missing name or
client ID as "does not match the search string". The claim page falls
back to a placeholder initial instead of indexing into `undefined`.

The authorize page now renders normally, minus the optional
partner-impersonation caution, which cannot be evaluated without a name.

Three changes:

-
`apps/studio/components/interfaces/Organization/OAuthApps/OAuthApps.utils.ts`
— `findTrustedPartnerByName` accepts `string | null | undefined` and
returns `null` early on a falsy name; `getOAuthImpersonationWarning`'s
`name` param widened to match (its existing `if (!namedPartner) return
null` already handles the rest).
- `apps/studio/components/interfaces/Auth/OAuthApps/oauthApps.utils.ts`
— `filterOAuthApps` optional-chains `client_name` and `client_id` before
`.toLowerCase()`, defaulting each match to `false`.
-
`apps/studio/components/interfaces/Organization/ProjectClaim/confirm.tsx`
— `{requester.name?.[0] ?? '?'}` for the fallback avatar initial.

Each is a separate commit so any one can be dropped independently.

## Additional context

### Root cause, not fixed here

`apps/studio/data/api-authorization/api-authorization-query.ts:37`
returns `data as ApiAuthorizationResponse`, an unchecked cast with no
runtime validation, even though the openapi-fetch client already types
the endpoint from the generated schema. Both the generated
`GetOAuthAuthorizationResponse` and the hand-written local type declare
`name: string` as required, so this was invisible to TypeScript.

The durable fix is to derive the type from the schema and drop the cast,
which is the house pattern elsewhere in `apps/studio/data`, and to
correct the OpenAPI spec at source if the API can legitimately omit
`name`. Left out deliberately to keep this cherry-pickable.

### Not in scope

`requester.scopes` is optional in the schema but required in the local
type, and is read unguarded in several places. Defaulting it to `[]`
would tell a user an app requested no permissions on a live consent
screen, so it needs a product decision rather than a drive-by guard.

### Testing

No local checks were run. This clone has no `node_modules` and `pnpm
install` is blocked in the environment, so `npm run build`, typecheck,
lint, Prettier and tests were all left to CI. Please treat CI as the
verification for this PR.

There is also a coverage gap worth noting:
`apps/studio/tests/components/ApiAuthorization.test.tsx:48-62` hardcodes
`name: 'Test App'` in `createMockAuthResponse`, and no test omits the
field, which is why none of these crashes were caught.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01P489vrPdHcJfMfzCGM9rZ5)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Ali Waseem <waseema393@gmail.com>
2026-08-19 14:23:54 -06:00
Danny White f7454cf94e feat(studio): oauth impersonation warning on authorize (#48162)
## 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 -->
2026-08-02 23:57:27 +00:00
Danny WhiteandJoshen Lim e19cd1863d feat(studio): connect logo contract for authorize (#48161)
## What kind of change does this PR introduce?

Feature + docs. Closes
[DEPR-604](https://linear.app/supabase/issue/DEPR-604/define-connect-logo-asset-and-variant-contract).

## What is the current behavior?

`/authorize` logo resolution trusted self-asserted requester `name` (and
similar) for curated MCP marks, fell back to a letter tile when there
was no usable icon, and always used theme-reactive tile chrome. This
includes the scenario when pairing against unclassified uploaded OAuth
app bitmaps.

## What is the new behavior?

- [Documents the Connect logo asset/variant
contract](https://design-system-git-danny-depr-604-connect-logo-contract-supabase.vercel.app/design-system/docs/ui-patterns/connect-interstitials#logos)
(default to light, keep pairs matched, no theme-recolour of vendor
SVGs).
- Resolves curated partner logos from allowlisted `redirect_uri` hosts
only (`claude.ai` / `anthropic.com`, `cursor.com` / `cursor.sh`,
`chatgpt.com` / `openai.com`, `perplexity.ai`).
- Unknown / missing / failed requester icons show `SupabaseLogo` alone
(no letter tile).
- Uploaded organisation OAuth app icons (unclassified bitmaps) pair with
fixed light tile chrome (`border-black/10 bg-white` / `SupabaseLogo
forceLight`) on both sides across Studio themes.
- Curated partners keep theme-reactive tiles and may use dark assets
when available.

### 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 once available).
2. Open `/dashboard/authorize?auth_id=foo` (any `auth_id` is fine — the
real response may 404).
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.

The fields that matter for this PR are `name`, `icon`, and
`redirect_uri`.

#### Curated pair (allowlisted redirect)

Expect Cursor mark + Supabase pair. Toggle light/dark: curated dark
assets may swap; tiles stay theme-reactive (`bg-surface-75`).

```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"
}
```

#### Unknown → Supabase alone

Expect Supabase bolt alone. No letter tile. No curated mark even if
`name` says Claude.

```json
{
  "name": "Acme",
  "website": "https://acme.example",
  "icon": null,
  "domain": "acme.example",
  "redirect_uri": "https://acme.example/callback",
  "expires_at": "2099-01-01T00:00:00.000Z",
  "scopes": ["organizations:read", "projects:read"],
  "approved_at": null,
  "registration_type": "dynamic"
}
```

#### Spoofed trusted name, non-allowlisted redirect (logo only)

Expect Supabase alone (no Claude mark). This PR does **not** show the
impersonation caution (that is coming in #48162).

```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"
}
```

#### Uploaded OAuth app icon → forced-light pair

Expect remote icon + Supabase pair with forced-light tiles
(`border-black/10 bg-white`) on both sides in light and dark Studio
themes. The icon URL below is the checked-in solid-colour Acme bitmap on
this branch.

```json
{
  "name": "Acme",
  "website": "https://acme.example",
  "icon": "https://raw.githubusercontent.com/supabase/supabase/danny/depr-604-connect-logo-contract/apps/design-system/public/img/icons/acme-oauth-icon.png",
  "domain": "acme.example",
  "redirect_uri": "https://acme.example/callback",
  "expires_at": "2099-01-01T00:00:00.000Z",
  "scopes": ["organizations:read", "projects:read"],
  "approved_at": null,
  "registration_type": "static"
}
```

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Improved authorization interstitial branding with trusted requester
logos and safer fallback behavior.
* Added support for consistent light-theme treatment of uploaded OAuth
app icons.
* Added examples and documentation for unknown requesters, uploaded
logos, and wrong-account states.
* **Bug Fixes**
* Prevented unverified or unavailable requester icons from being
presented as trusted.
* Ensured logo pairing remains visually consistent across light and dark
themes.
* **Tests**
* Added coverage for trusted-host validation, fallback branding, icon
loading failures, and theme behavior.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2026-07-23 02:20:42 +10:00