Skip to content

feat(ui,react): Introduce OAuthConsent component#8289

Merged
wobsoriano merged 44 commits intomainfrom
rob/oauth-consent
Apr 14, 2026
Merged

feat(ui,react): Introduce OAuthConsent component#8289
wobsoriano merged 44 commits intomainfrom
rob/oauth-consent

Conversation

@wobsoriano
Copy link
Copy Markdown
Member

@wobsoriano wobsoriano commented Apr 11, 2026

Description

Summary

  • Refactors clerk.oauthApplication from a hollow static resource class to a proper module class (matching the apiKeys and billing patterns), and adds buildConsentActionUrl({ clientId }) so custom-flow developers can get the consent form's POST URL without constructing it manually
  • Exports a <OAuthConsent /> component and a useOAuthConsent hook from the /internal path for now

<OAuthConsent /> renders the consent screen using useOAuthConsent to fetch scope and application metadata from FAPI when client_id and redirect_uri are provided as URL parameters.

This update is backwards compatible with accounts portal consent page. Once ready, we'll update accounts portal to use the component.

Screenshot 2026-04-13 at 3 43 08 PM

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

Introduce a zero-config <OAuthConsent /> React component exported from
@clerk/react and @clerk/nextjs that renders the OAuth consent screen for a
signed-in user. The component reads client_id, scope, and redirect_uri from
the URL by default and submits the consent decision via a native form POST
to /v1/internal/oauth-consent.

The accounts portal continues to work unchanged via the existing
clerk.__internal_mountOAuthConsent path; the underlying UI component is a
hybrid that uses context values when provided (accounts portal path) and
falls back to the useOAuthConsent hook + URL parsing otherwise (public path).

Highlights:
- New public OAuthConsentProps type in @clerk/shared
- OAuthConsentCtx refactored to lowercase oauth* casing with translation
  layer in ComponentContextProvider for accounts portal backward compat
- Hybrid _OAuthConsent component with native form wrapping and submit
  buttons (proper a11y semantics)
- URL parsing moved out of useOAuthConsent hook into the component for
  cleaner separation of concerns and SSR safety
- New <OAuthConsent /> wrapper in @clerk/react re-exported from @clerk/nextjs
- 7 unit tests covering public and accounts portal paths
- Export snapshot updates for react-router and tanstack-react-start
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Apr 14, 2026 8:57pm

Request Review

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 11, 2026

🦋 Changeset detected

Latest commit: c93b163

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 21 packages
Name Type
@clerk/nextjs Minor
@clerk/react Minor
@clerk/shared Minor
@clerk/ui Minor
@clerk/tanstack-react-start Minor
@clerk/react-router Minor
@clerk/localizations Minor
@clerk/chrome-extension Patch
@clerk/expo Patch
@clerk/agent-toolkit Patch
@clerk/astro Patch
@clerk/backend Patch
@clerk/clerk-js Patch
@clerk/expo-passkeys Patch
@clerk/express Patch
@clerk/fastify Patch
@clerk/hono Patch
@clerk/msw Patch
@clerk/nuxt Patch
@clerk/testing Patch
@clerk/vue Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 11, 2026

Open in StackBlitz

@clerk/agent-toolkit

npm i https://pkg.pr.new/@clerk/agent-toolkit@8289

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@8289

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@8289

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@8289

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@8289

@clerk/dev-cli

npm i https://pkg.pr.new/@clerk/dev-cli@8289

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@8289

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@8289

@clerk/express

npm i https://pkg.pr.new/@clerk/express@8289

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@8289

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@8289

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@8289

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@8289

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@8289

@clerk/react

npm i https://pkg.pr.new/@clerk/react@8289

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@8289

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@8289

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@8289

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@8289

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@8289

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@8289

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@8289

commit: c93b163

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 11, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 7751eee5-9e20-4e5f-95a0-63dfcaf3f3f6

📥 Commits

Reviewing files that changed from the base of the PR and between e7b9287 and c93b163.

📒 Files selected for processing (1)
  • .changeset/public-oauth-consent-component.md
✅ Files skipped from review due to trivial changes (1)
  • .changeset/public-oauth-consent-component.md

📝 Walkthrough

Walkthrough

Adds a new OAuthConsent component and re-exports it across @clerk/react, @clerk/nextjs, react-router, and tanstack-react-start internal entry points. Introduces UI primitives (LogoGroup, ListGroup, InlineAction, OrgSelect), OAuthConsent URL utilities, localization and appearance keys, and context/type adjustments for OAuth consent props. Changes useOAuthConsent to require explicit params (removes URL fallback). Refactors OAuthApplication into an instantiable class with instance getConsentInfo and buildConsentActionUrl and updates Clerk core wiring and resource exports. Adds many tests for the new UI and OAuthApplication, and removes several legacy tests and sandbox adjustments.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main feature introduced in this changeset: the new OAuthConsent component.
Description check ✅ Passed The description is directly related to the changeset, explaining the refactoring of clerk.oauthApplication, new component exports, and backwards compatibility with accounts portal.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/ui/src/components/OAuthConsent/OAuthConsent.tsx`:
- Around line 49-77: The form currently intercepts all submissions when either
ctx.onAllow or ctx.onDeny exists causing the missing callback to become a no-op;
change the gating logic to require both callbacks be present: replace
hasContextCallbacks = Boolean(ctx.onAllow || ctx.onDeny) with a check like
hasBothContextCallbacks = Boolean(ctx.onAllow && ctx.onDeny) and use
hasBothContextCallbacks in handleSubmit and in the code that suppresses the
hidden fallback inputs (the block around the hidden fallback inputs currently at
lines ~314-322) so that when only one callback is provided the other button
falls back to native POST.
- Around line 51-58: The actionUrl in OAuthConsent is missing forwarding of the
dev-browser JWT for development frontends, causing unauthenticated POSTs in dev;
modify the actionUrl builder in OAuthConsent so when the target is a dev
frontend (detect via clerk.frontendApi containing localhost/127.0.0.1 or a
CLERK_DEV_FRONTEND env flag), append a query param (e.g. dev_browser_jwt) with
the development JWT obtained from clerk (e.g. clerk.devBrowserJWT) or by calling
a helper like getDevBrowserJWT() before returning url.toString(), and keep the
existing session id param logic intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: e9b4495f-f3d8-4368-a9ff-f11a6dcb0edd

📥 Commits

Reviewing files that changed from the base of the PR and between f9ff9e9 and 12b1ebb.

⛔ Files ignored due to path filters (2)
  • packages/react-router/src/__tests__/__snapshots__/exports.test.ts.snap is excluded by !**/*.snap
  • packages/tanstack-react-start/src/__tests__/__snapshots__/exports.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (15)
  • .changeset/public-oauth-consent-component.md
  • packages/nextjs/src/client-boundary/uiComponents.tsx
  • packages/nextjs/src/index.ts
  • packages/react/src/components/index.ts
  • packages/react/src/components/uiComponents.tsx
  • packages/shared/src/react/hooks/__tests__/useOAuthConsent.shared.spec.ts
  • packages/shared/src/react/hooks/__tests__/useOAuthConsent.spec.tsx
  • packages/shared/src/react/hooks/useOAuthConsent.shared.ts
  • packages/shared/src/react/hooks/useOAuthConsent.tsx
  • packages/shared/src/react/hooks/useOAuthConsent.types.ts
  • packages/shared/src/types/clerk.ts
  • packages/ui/src/components/OAuthConsent/OAuthConsent.tsx
  • packages/ui/src/components/OAuthConsent/__tests__/OAuthConsent.spec.tsx
  • packages/ui/src/contexts/ClerkUIComponentsContext.tsx
  • packages/ui/src/types.ts
💤 Files with no reviewable changes (3)
  • packages/shared/src/react/hooks/useOAuthConsent.shared.ts
  • packages/shared/src/react/hooks/tests/useOAuthConsent.shared.spec.ts
  • packages/shared/src/react/hooks/tests/useOAuthConsent.spec.tsx

Comment thread packages/ui/src/components/OAuthConsent/OAuthConsent.tsx Outdated
Comment thread packages/ui/src/components/OAuthConsent/OAuthConsent.tsx Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
packages/ui/src/components/OAuthConsent/OAuthConsent.tsx (2)

49-53: ⚠️ Potential issue | 🔴 Critical

Partial callback mode breaks one consent action.

Line 49 treats “either callback exists” as full context mode. Then Line 94 always prevents native submit, and Line 341 suppresses hidden fallback params. If only one callback is provided, the other button becomes a no-op.

Suggested fix
- const hasContextCallbacks = Boolean(ctx.onAllow || ctx.onDeny);
+ const hasAllowCallback = Boolean(ctx.onAllow);
+ const hasDenyCallback = Boolean(ctx.onDeny);
+ const hasBothContextCallbacks = hasAllowCallback && hasDenyCallback;

- const isPublicFlow = !hasContextCallbacks;
+ const isPublicFlow = !hasBothContextCallbacks;

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
-   if (!hasContextCallbacks) {
-     return;
-   }
-   e.preventDefault();
    const submitter = (e.nativeEvent as SubmitEvent).submitter as HTMLButtonElement | null;
-   if (submitter?.value === 'true') {
-     ctx.onAllow?.();
-   } else {
-     ctx.onDeny?.();
-   }
+   const callback = submitter?.value === 'true' ? ctx.onAllow : ctx.onDeny;
+   if (!callback) {
+     return;
+   }
+   e.preventDefault();
+   callback();
  };

- {!hasContextCallbacks &&
+ {!hasBothContextCallbacks &&
    forwardedParams.map(([key, value]) => (
      <input key={key} type='hidden' name={key} value={value} />
    ))}

As per coding guidelines, “Highlight only issues that could cause runtime errors, data loss, or severe maintainability issues.”

Also applies to: 93-104, 341-349

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/components/OAuthConsent/OAuthConsent.tsx` around lines 49 -
53, The code treats "hasContextCallbacks" as true if either ctx.onAllow or
ctx.onDeny exists, which breaks the other action when only one callback is
provided; change the logic to require both callbacks (use both ctx.onAllow &&
ctx.onDeny) when deciding full-context behavior (adjust
hasContextCallbacks/isPublicFlow accordingly), update the submit handler (the
function that currently always prevents native submission) to only prevent
default when both callbacks are present, and stop suppressing the hidden
fallback params unless both callbacks exist so the fallback submit will work
when one callback is missing.

78-85: ⚠️ Potential issue | 🔴 Critical

Native POST auth is incomplete for development instances.

Line 83 explicitly leaves dev JWT forwarding as TODO. This keeps the dev consent POST path potentially unauthenticated and failing in development environments.

#!/bin/bash
set -euo pipefail

# Verify whether repo already has a canonical helper/field for dev browser JWT forwarding.
rg -n -C3 --type=ts --type=tsx 'dev_browser_jwt|devBrowserJWT|getDevBrowserJWT|CLERK_DEV_FRONTEND|frontendApi'

# Verify all OAuth consent POST action builders for consistency.
rg -n -C3 --type=ts --type=tsx '/v1/internal/oauth-consent'

As per coding guidelines, “Comment only when the issue must be resolved before merge, otherwise remain silent.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/components/OAuthConsent/OAuthConsent.tsx` around lines 78 -
85, The actionUrl builder currently omits forwarding the dev browser JWT,
causing dev POSTs to /v1/internal/oauth-consent to be unauthenticated; update
the IIFE that constructs actionUrl to detect development instances and append
the dev JWT (e.g., use the project's canonical helper such as getDevBrowserJWT
or devBrowserJWT if present, or read CLERK_DEV_FRONTEND token) as a query param
(e.g., dev_browser_jwt) when available, ensuring the URL includes
url.searchParams.set('_clerk_dev_browser_jwt', token) or the agreed param name;
reference the actionUrl constant and clerk.session usage so you add the token
only in dev contexts and do not change production behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/ui/src/components/OAuthConsent/OAuthConsent.tsx`:
- Around line 49-53: The code treats "hasContextCallbacks" as true if either
ctx.onAllow or ctx.onDeny exists, which breaks the other action when only one
callback is provided; change the logic to require both callbacks (use both
ctx.onAllow && ctx.onDeny) when deciding full-context behavior (adjust
hasContextCallbacks/isPublicFlow accordingly), update the submit handler (the
function that currently always prevents native submission) to only prevent
default when both callbacks are present, and stop suppressing the hidden
fallback params unless both callbacks exist so the fallback submit will work
when one callback is missing.
- Around line 78-85: The actionUrl builder currently omits forwarding the dev
browser JWT, causing dev POSTs to /v1/internal/oauth-consent to be
unauthenticated; update the IIFE that constructs actionUrl to detect development
instances and append the dev JWT (e.g., use the project's canonical helper such
as getDevBrowserJWT or devBrowserJWT if present, or read CLERK_DEV_FRONTEND
token) as a query param (e.g., dev_browser_jwt) when available, ensuring the URL
includes url.searchParams.set('_clerk_dev_browser_jwt', token) or the agreed
param name; reference the actionUrl constant and clerk.session usage so you add
the token only in dev contexts and do not change production behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: b8ad3739-da04-4a58-860a-7f698aadc4dc

📥 Commits

Reviewing files that changed from the base of the PR and between 31a55e9 and e093465.

📒 Files selected for processing (1)
  • packages/ui/src/components/OAuthConsent/OAuthConsent.tsx

@github-actions

This comment has been minimized.

…lic flow

Remove the "Authorization failed:" prefix from error messages to match
the rest of the codebase's direct-message convention.

Add a loading guard so the consent card does not render with empty values
while the useOAuthConsent hook is still fetching. Returns null until data
is available, letting the React wrapper's fallback handle loading UI.
…onsentProps alongside deprecated capital-A ones

Both casings coexist: new lowercase fields (oauthApplicationName, etc.)
are preferred, while the deprecated capital-A fields (oAuthApplicationName,
etc.) remain for accounts portal backward compat. The translation layer
in ComponentContextProvider uses the new fields with ?? fallback to the
deprecated ones.
…entProps to AvailableComponentProps

Gate useOAuthConsent with enabled: !hasContextCallbacks so the hook
skips the FAPI request when the accounts portal already provides all
data via context. Add test assertion confirming no fetch on that path.

Add __internal_OAuthConsentProps to the AvailableComponentProps union
for type hygiene and consistency with other internal component props.
@wobsoriano
Copy link
Copy Markdown
Member Author

!snapshot

@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
packages/ui/src/components/OAuthConsent/OAuthConsent.tsx (1)

55-57: ⚠️ Potential issue | 🔴 Critical

Require both context callbacks before switching out of the native POST flow.

A single onAllow or onDeny is enough to disable the public-flow fetch, suppress the hidden fallback inputs, and preventDefault() every submit. If a caller provides only one callback, the other button becomes a no-op instead of falling back to the native consent POST.

Suggested fix
-  // onAllow and onDeny are always provided as a pair by the accounts portal.
-  const hasContextCallbacks = Boolean(ctx.onAllow || ctx.onDeny);
+  const hasBothContextCallbacks = Boolean(ctx.onAllow && ctx.onDeny);
...
   const { data, isLoading, error } = useOAuthConsent({
     oauthClientId,
     scope,
     // TODO: Remove this once account portal is refactored to use this component
-    enabled: !hasContextCallbacks,
+    enabled: !hasBothContextCallbacks,
   });
...
   const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
-    if (!hasContextCallbacks) {
-      return;
-    }
-    e.preventDefault();
     const submitter = (e.nativeEvent as SubmitEvent).submitter as HTMLButtonElement | null;
-    if (submitter?.value === 'true') {
-      ctx.onAllow?.();
-    } else {
-      ctx.onDeny?.();
+    const callback = submitter?.value === 'true' ? ctx.onAllow : ctx.onDeny;
+    if (!callback) {
+      return;
     }
+    e.preventDefault();
+    callback();
   };
...
-        {!hasContextCallbacks &&
+        {!hasBothContextCallbacks &&
           forwardedParams.map(([key, value]) => (
             <input
               key={key}
               type='hidden'
               name={key}
               value={value}
             />
           ))}
-        {!hasContextCallbacks && ctx.enableOrgSelection && selectedOrg && (
+        {!hasBothContextCallbacks && ctx.enableOrgSelection && selectedOrg && (
           <input
             type='hidden'
             name='organization_id'
             value={selectedOrg}
           />
         )}

Also applies to: 65-70, 131-141, 299-314

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/components/OAuthConsent/OAuthConsent.tsx` around lines 55 -
57, The current logic treats the presence of either ctx.onAllow or ctx.onDeny as
sufficient to disable the native POST flow; change this to require both
callbacks be provided before switching flows by replacing Boolean(ctx.onAllow ||
ctx.onDeny) with Boolean(ctx.onAllow && ctx.onDeny) (update the
hasContextCallbacks variable and any other places that check these callbacks),
and use that stricter condition in the form submit handler (where preventDefault
is called), in the rendering decision for the hidden fallback inputs, and in the
button click handlers so that if only one callback is present the component
remains in the native POST flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/ui/src/components/OAuthConsent/utils.ts`:
- Around line 3-6: getRootDomain currently naively returns the last two labels
of the hostname which fails for multi-part public suffixes (e.g. *.co.uk),
IPv4/IPv6 literals, and localhost; update getRootDomain to parse the hostname
via the URL constructor, detect IP literals or single-label hosts and return
hostname as-is for those cases, and otherwise use a Public Suffix List-aware
resolver (e.g., the "psl" package) to compute the registered domain
(psl.get(hostname)) and fall back to hostname if psl returns null. Ensure the
function still throws only on invalid URLs and reference getRootDomain when
applying these changes.

---

Duplicate comments:
In `@packages/ui/src/components/OAuthConsent/OAuthConsent.tsx`:
- Around line 55-57: The current logic treats the presence of either ctx.onAllow
or ctx.onDeny as sufficient to disable the native POST flow; change this to
require both callbacks be provided before switching flows by replacing
Boolean(ctx.onAllow || ctx.onDeny) with Boolean(ctx.onAllow && ctx.onDeny)
(update the hasContextCallbacks variable and any other places that check these
callbacks), and use that stricter condition in the form submit handler (where
preventDefault is called), in the rendering decision for the hidden fallback
inputs, and in the button click handlers so that if only one callback is present
the component remains in the native POST flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 0009687f-0a8e-44c5-ba5d-75335974666a

📥 Commits

Reviewing files that changed from the base of the PR and between 9b72192 and bea3330.

📒 Files selected for processing (34)
  • .changeset/public-oauth-consent-component.md
  • docs/superpowers/specs/2026-04-13-oauth-consent-org-selection.md
  • packages/clerk-js/sandbox/app.ts
  • packages/clerk-js/src/core/clerk.ts
  • packages/clerk-js/src/core/modules/oauthApplication/__tests__/OAuthApplication.test.ts
  • packages/clerk-js/src/core/modules/oauthApplication/index.ts
  • packages/clerk-js/src/core/resources/__tests__/OAuthApplication.test.ts
  • packages/clerk-js/src/core/resources/internal.ts
  • packages/localizations/src/en-US.ts
  • packages/nextjs/src/client-boundary/uiComponents.tsx
  • packages/nextjs/src/internal.ts
  • packages/react-router/package.json
  • packages/react-router/src/internal.ts
  • packages/react/src/components/uiComponents.tsx
  • packages/react/src/internal.ts
  • packages/shared/src/react/hooks/useOAuthConsent.tsx
  • packages/shared/src/react/hooks/useOAuthConsent.types.ts
  • packages/shared/src/types/clerk.ts
  • packages/shared/src/types/localization.ts
  • packages/shared/src/types/oauthApplication.ts
  • packages/tanstack-react-start/package.json
  • packages/tanstack-react-start/src/internal.ts
  • packages/ui/src/components/OAuthConsent/InlineAction.tsx
  • packages/ui/src/components/OAuthConsent/ListGroup.tsx
  • packages/ui/src/components/OAuthConsent/LogoGroup.tsx
  • packages/ui/src/components/OAuthConsent/OAuthConsent.tsx
  • packages/ui/src/components/OAuthConsent/OrgSelect.tsx
  • packages/ui/src/components/OAuthConsent/__tests__/InlineAction.test.tsx
  • packages/ui/src/components/OAuthConsent/__tests__/OAuthConsent.test.tsx
  • packages/ui/src/components/OAuthConsent/utils.ts
  • packages/ui/src/contexts/ClerkUIComponentsContext.tsx
  • packages/ui/src/customizables/elementDescriptors.ts
  • packages/ui/src/internal/appearance.ts
  • packages/ui/src/types.ts
💤 Files with no reviewable changes (2)
  • packages/clerk-js/src/core/resources/internal.ts
  • packages/clerk-js/src/core/resources/tests/OAuthApplication.test.ts
✅ Files skipped from review due to trivial changes (4)
  • packages/react-router/src/internal.ts
  • packages/nextjs/src/internal.ts
  • packages/tanstack-react-start/src/internal.ts
  • packages/localizations/src/en-US.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • packages/nextjs/src/client-boundary/uiComponents.tsx
  • packages/shared/src/react/hooks/useOAuthConsent.types.ts
  • packages/shared/src/react/hooks/useOAuthConsent.tsx
  • packages/ui/src/contexts/ClerkUIComponentsContext.tsx
  • packages/react/src/components/uiComponents.tsx
  • .changeset/public-oauth-consent-component.md
  • packages/ui/src/types.ts
  • packages/shared/src/types/clerk.ts

Comment on lines +3 to +6
export function getRootDomain(url: string): string {
try {
const { hostname } = new URL(url);
return hostname.split('.').slice(-2).join('.');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

getRootDomain can display an incorrect destination host in consent warnings

On Line [6], taking only the last two hostname labels breaks for multi-part public suffixes (*.co.uk) and IP hosts, which can show misleading domain text in a trust/security prompt.

Suggested fix
 export function getRootDomain(url: string): string {
   try {
-    const { hostname } = new URL(url);
-    return hostname.split('.').slice(-2).join('.');
+    // Use the parsed hostname directly to avoid incorrect truncation
+    // for multi-part TLDs and IP literals.
+    return new URL(url).hostname;
   } catch {
     return '';
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function getRootDomain(url: string): string {
try {
const { hostname } = new URL(url);
return hostname.split('.').slice(-2).join('.');
export function getRootDomain(url: string): string {
try {
// Use the parsed hostname directly to avoid incorrect truncation
// for multi-part TLDs and IP literals.
return new URL(url).hostname;
} catch {
return '';
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/components/OAuthConsent/utils.ts` around lines 3 - 6,
getRootDomain currently naively returns the last two labels of the hostname
which fails for multi-part public suffixes (e.g. *.co.uk), IPv4/IPv6 literals,
and localhost; update getRootDomain to parse the hostname via the URL
constructor, detect IP literals or single-label hosts and return hostname as-is
for those cases, and otherwise use a Public Suffix List-aware resolver (e.g.,
the "psl" package) to compute the registered domain (psl.get(hostname)) and fall
back to hostname if psl returns null. Ensure the function still throws only on
invalid URLs and reference getRootDomain when applying these changes.

@wobsoriano
Copy link
Copy Markdown
Member Author

!snapshot

@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/ui/src/components/OAuthConsent/OAuthConsent.tsx`:
- Around line 136-146: handleSubmit prevents native form submission and calls
ctx.onAllow/onDeny without passing the selected organization, so
ctx.enableOrgSelection changes (selectedOrg/effectiveOrg) never reach the
callback-backed flow; either restore native submission or modify the callback
contract and invocations to include the effective org. Update handleSubmit to
compute the selected org (same logic as where selected org is serialized for
POST) and pass it into ctx.onAllow(effectiveOrg) / ctx.onDeny(effectiveOrg) (and
update the ctx type for onAllow/onDeny accordingly), or alternatively stop
calling e.preventDefault() and allow the existing form POST to include
organization_id; ensure the same fix is applied to the other referenced places
that trigger consent via callbacks (the org selection logic around
selectedOrg/effectiveOrg and the serialization used at the POST path).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: f2de1501-201e-4126-88a0-0ab01497224c

📥 Commits

Reviewing files that changed from the base of the PR and between bea3330 and 393ac8f.

📒 Files selected for processing (1)
  • packages/ui/src/components/OAuthConsent/OAuthConsent.tsx

Comment on lines +136 to +146
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
if (!hasContextCallbacks) {
return;
}
e.preventDefault();
const submitter = (e.nativeEvent as SubmitEvent).submitter as HTMLButtonElement | null;
if (submitter?.value === 'true') {
ctx.onAllow?.();
} else {
ctx.onDeny?.();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

organization_id is ignored on the callback-backed flow.

When ctx.enableOrgSelection is on, the user can change the org at Lines 228-233, but that value only gets serialized at Lines 313-318. In the accounts-portal path, handleSubmit at Lines 136-146 prevents the native POST and calls ctx.onAllow?.() / ctx.onDeny?.() with no org payload, so the selected org never affects the consent action. Either keep native submission here or extend the callback contract to carry effectiveOrg.

Also applies to: 228-233, 313-318

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/components/OAuthConsent/OAuthConsent.tsx` around lines 136 -
146, handleSubmit prevents native form submission and calls ctx.onAllow/onDeny
without passing the selected organization, so ctx.enableOrgSelection changes
(selectedOrg/effectiveOrg) never reach the callback-backed flow; either restore
native submission or modify the callback contract and invocations to include the
effective org. Update handleSubmit to compute the selected org (same logic as
where selected org is serialized for POST) and pass it into
ctx.onAllow(effectiveOrg) / ctx.onDeny(effectiveOrg) (and update the ctx type
for onAllow/onDeny accordingly), or alternatively stop calling
e.preventDefault() and allow the existing form POST to include organization_id;
ensure the same fix is applied to the other referenced places that trigger
consent via callbacks (the org selection logic around selectedOrg/effectiveOrg
and the serialization used at the POST path).

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
packages/ui/src/components/OAuthConsent/OAuthConsent.tsx (2)

50-52: ⚠️ Potential issue | 🔴 Critical

Callback-backed consent still drops the selected organization.

effectiveOrg changes when the user picks a different org, but Lines 148-151 invoke the consent callbacks without that value, and Lines 319-324 only serialize organization_id for the native POST path. In the accounts-portal flow, the org selector therefore has no effect on the submitted consent.

Also applies to: 142-152, 234-239, 319-324

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/components/OAuthConsent/OAuthConsent.tsx` around lines 50 -
52, The selected organization (selectedOrg/effectiveOrg) is not being passed
into the consent callbacks or serialized for the callback-backed/accounts-portal
path; update the places that invoke the consent handlers (onConsent / onDeny or
whatever consent callback functions are used around the consent submission
logic) to pass effectiveOrg (or its id) as an argument, and ensure the consent
payload generation logic also includes organization_id when building the
POST/callback payload for the callback-backed/accounts-portal flow so the
selected org is sent in both native POST and callback-backed paths.

53-55: ⚠️ Potential issue | 🔴 Critical

Require both consent callbacks before hijacking the form.

Lines 53-55 switch into callback mode when either handler exists, but Lines 142-152 then block every submit and Lines 310-325 remove the native POST fallback inputs. If only one callback is provided, the other button becomes a no-op.

Also applies to: 142-152, 310-325

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/components/OAuthConsent/OAuthConsent.tsx` around lines 53 -
55, The consent form currently hijacks native POST fallback when only one of the
portal callbacks is present because hasContextCallbacks is computed with ||;
change it to require both callbacks (use ctx.onAllow && ctx.onDeny) so the form
only switches to callback mode when both handlers exist, and update the related
submit interception logic (the submit handler that blocks submits) and the code
path that removes native POST fallback inputs (the rendering branch that strips
hidden inputs) to be gated by the updated hasContextCallbacks boolean (or
equivalent check) so the deny/allow buttons remain functional if either callback
is missing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/ui/src/components/OAuthConsent/OAuthConsent.tsx`:
- Around line 50-52: The selected organization (selectedOrg/effectiveOrg) is not
being passed into the consent callbacks or serialized for the
callback-backed/accounts-portal path; update the places that invoke the consent
handlers (onConsent / onDeny or whatever consent callback functions are used
around the consent submission logic) to pass effectiveOrg (or its id) as an
argument, and ensure the consent payload generation logic also includes
organization_id when building the POST/callback payload for the
callback-backed/accounts-portal flow so the selected org is sent in both native
POST and callback-backed paths.
- Around line 53-55: The consent form currently hijacks native POST fallback
when only one of the portal callbacks is present because hasContextCallbacks is
computed with ||; change it to require both callbacks (use ctx.onAllow &&
ctx.onDeny) so the form only switches to callback mode when both handlers exist,
and update the related submit interception logic (the submit handler that blocks
submits) and the code path that removes native POST fallback inputs (the
rendering branch that strips hidden inputs) to be gated by the updated
hasContextCallbacks boolean (or equivalent check) so the deny/allow buttons
remain functional if either callback is missing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 4e92224f-1baa-4ff5-b60f-c7e8df18b127

📥 Commits

Reviewing files that changed from the base of the PR and between 68d538c and c49e397.

📒 Files selected for processing (1)
  • packages/ui/src/components/OAuthConsent/OAuthConsent.tsx

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
packages/ui/src/components/OAuthConsent/OAuthConsent.tsx (2)

50-51: ⚠️ Potential issue | 🔴 Critical

Partial context callbacks still break one consent action.

Using || means if only one callback is provided, the other button becomes a no-op (form submission prevented, but no callback invoked). Per the OAuthConsentCtx JSDoc, both callbacks are expected together. Should use &&:

-  const hasContextCallbacks = Boolean(ctx.onAllow || ctx.onDeny);
+  const hasContextCallbacks = Boolean(ctx.onAllow && ctx.onDeny);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/components/OAuthConsent/OAuthConsent.tsx` around lines 50 -
51, The current hasContextCallbacks uses Boolean(ctx.onAllow || ctx.onDeny)
which treats a single provided callback as valid and causes the other consent
action to become a silent no-op; change the check to require both callbacks (use
ctx.onAllow && ctx.onDeny or Boolean(ctx.onAllow && ctx.onDeny)) so
hasContextCallbacks accurately reflects the OAuthConsentCtx expectation that
onAllow and onDeny are provided together and both buttons invoke their
callbacks.

139-150: ⚠️ Potential issue | 🔴 Critical

organization_id is still lost on the callback-backed flow.

When ctx.enableOrgSelection is enabled, the user's org selection (effectiveOrg) is never passed to ctx.onAllow() / ctx.onDeny(). The callback signature should be extended to accept the selected org, or the form should POST natively.

   const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
     if (!hasContextCallbacks) {
       return;
     }
     e.preventDefault();
     const submitter = (e.nativeEvent as SubmitEvent).submitter as HTMLButtonElement | null;
     if (submitter?.value === 'true') {
-      ctx.onAllow?.();
+      ctx.onAllow?.(effectiveOrg);
     } else {
-      ctx.onDeny?.();
+      ctx.onDeny?.(effectiveOrg);
     }
   };

This also requires updating the OAuthConsentCtx type to accept the org parameter.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/components/OAuthConsent/OAuthConsent.tsx` around lines 139 -
150, The handleSubmit currently calls ctx.onAllow() / ctx.onDeny() without
passing the selected organization, losing organization_id when
enableOrgSelection is used; update the OAuthConsentCtx type to change onAllow
and onDeny to accept the selected org (e.g., onAllow?: (effectiveOrg?: string)
=> void) and then modify the handleSubmit function to pass the current
effectiveOrg value into ctx.onAllow(effectiveOrg) / ctx.onDeny(effectiveOrg);
also search for other places that construct or call OAuthConsentCtx callbacks
and update their call sites to handle the new org parameter.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/ui/src/components/OAuthConsent/OAuthConsent.tsx`:
- Around line 50-51: The current hasContextCallbacks uses Boolean(ctx.onAllow ||
ctx.onDeny) which treats a single provided callback as valid and causes the
other consent action to become a silent no-op; change the check to require both
callbacks (use ctx.onAllow && ctx.onDeny or Boolean(ctx.onAllow && ctx.onDeny))
so hasContextCallbacks accurately reflects the OAuthConsentCtx expectation that
onAllow and onDeny are provided together and both buttons invoke their
callbacks.
- Around line 139-150: The handleSubmit currently calls ctx.onAllow() /
ctx.onDeny() without passing the selected organization, losing organization_id
when enableOrgSelection is used; update the OAuthConsentCtx type to change
onAllow and onDeny to accept the selected org (e.g., onAllow?: (effectiveOrg?:
string) => void) and then modify the handleSubmit function to pass the current
effectiveOrg value into ctx.onAllow(effectiveOrg) / ctx.onDeny(effectiveOrg);
also search for other places that construct or call OAuthConsentCtx callbacks
and update their call sites to handle the new org parameter.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 5880c230-2fdb-4bee-8148-5da46918865b

📥 Commits

Reviewing files that changed from the base of the PR and between c49e397 and e7b9287.

📒 Files selected for processing (1)
  • packages/ui/src/components/OAuthConsent/OAuthConsent.tsx

@github-actions
Copy link
Copy Markdown
Contributor

Hey @wobsoriano - the snapshot version command generated the following package versions:

Package Version
@clerk/agent-toolkit 0.3.14-snapshot.v20260414205636
@clerk/astro 3.0.14-snapshot.v20260414205636
@clerk/backend 3.2.10-snapshot.v20260414205636
@clerk/chrome-extension 3.1.11-snapshot.v20260414205636
@clerk/clerk-js 6.7.1-snapshot.v20260414205636
@clerk/dev-cli 0.1.1-snapshot.v20260414205636
@clerk/expo 3.1.11-snapshot.v20260414205636
@clerk/expo-passkeys 1.0.12-snapshot.v20260414205636
@clerk/express 2.1.2-snapshot.v20260414205636
@clerk/fastify 3.1.12-snapshot.v20260414205636
@clerk/hono 0.1.12-snapshot.v20260414205636
@clerk/localizations 4.4.2-snapshot.v20260414205636
@clerk/msw 0.0.12-snapshot.v20260414205636
@clerk/nextjs 7.2.0-snapshot.v20260414205636
@clerk/nuxt 2.2.1-snapshot.v20260414205636
@clerk/react 6.4.0-snapshot.v20260414205636
@clerk/react-router 3.1.0-snapshot.v20260414205636
@clerk/shared 4.8.0-snapshot.v20260414205636
@clerk/tanstack-react-start 1.1.0-snapshot.v20260414205636
@clerk/testing 2.0.14-snapshot.v20260414205636
@clerk/ui 1.6.0-snapshot.v20260414205636
@clerk/upgrade 2.0.3-snapshot.v20260414205636
@clerk/vue 2.0.13-snapshot.v20260414205636

Tip: Use the snippet copy button below to quickly install the required packages.
@clerk/agent-toolkit

npm i @clerk/agent-toolkit@0.3.14-snapshot.v20260414205636 --save-exact

@clerk/astro

npm i @clerk/astro@3.0.14-snapshot.v20260414205636 --save-exact

@clerk/backend

npm i @clerk/backend@3.2.10-snapshot.v20260414205636 --save-exact

@clerk/chrome-extension

npm i @clerk/chrome-extension@3.1.11-snapshot.v20260414205636 --save-exact

@clerk/clerk-js

npm i @clerk/clerk-js@6.7.1-snapshot.v20260414205636 --save-exact

@clerk/dev-cli

npm i @clerk/dev-cli@0.1.1-snapshot.v20260414205636 --save-exact

@clerk/expo

npm i @clerk/expo@3.1.11-snapshot.v20260414205636 --save-exact

@clerk/expo-passkeys

npm i @clerk/expo-passkeys@1.0.12-snapshot.v20260414205636 --save-exact

@clerk/express

npm i @clerk/express@2.1.2-snapshot.v20260414205636 --save-exact

@clerk/fastify

npm i @clerk/fastify@3.1.12-snapshot.v20260414205636 --save-exact

@clerk/hono

npm i @clerk/hono@0.1.12-snapshot.v20260414205636 --save-exact

@clerk/localizations

npm i @clerk/localizations@4.4.2-snapshot.v20260414205636 --save-exact

@clerk/msw

npm i @clerk/msw@0.0.12-snapshot.v20260414205636 --save-exact

@clerk/nextjs

npm i @clerk/nextjs@7.2.0-snapshot.v20260414205636 --save-exact

@clerk/nuxt

npm i @clerk/nuxt@2.2.1-snapshot.v20260414205636 --save-exact

@clerk/react

npm i @clerk/react@6.4.0-snapshot.v20260414205636 --save-exact

@clerk/react-router

npm i @clerk/react-router@3.1.0-snapshot.v20260414205636 --save-exact

@clerk/shared

npm i @clerk/shared@4.8.0-snapshot.v20260414205636 --save-exact

@clerk/tanstack-react-start

npm i @clerk/tanstack-react-start@1.1.0-snapshot.v20260414205636 --save-exact

@clerk/testing

npm i @clerk/testing@2.0.14-snapshot.v20260414205636 --save-exact

@clerk/ui

npm i @clerk/ui@1.6.0-snapshot.v20260414205636 --save-exact

@clerk/upgrade

npm i @clerk/upgrade@2.0.3-snapshot.v20260414205636 --save-exact

@clerk/vue

npm i @clerk/vue@2.0.13-snapshot.v20260414205636 --save-exact

@wobsoriano wobsoriano merged commit dc2de16 into main Apr 14, 2026
42 checks passed
@wobsoriano wobsoriano deleted the rob/oauth-consent branch April 14, 2026 21:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants