Skip to main content

Clerk strongly recommends using the default OAuth consent page hosted by the Account Portal. OAuth consent is a security boundary: it is where a signed-in user decides whether an OAuth can receive and access the requested data. A custom page can weaken that boundary if it hides the requesting application, misstates the requested scopes, buries the deny action, auto-approves access, or trains users to trust an unfamiliar consent surface.

Only set up a custom OAuth consent page when you have a specific product requirement that the Account Portal cannot satisfy. If you only need visual customization, see Use appearance for visual changes. If you do need a custom page, there are two ways to do this:

After you create and deploy your custom consent route, you'll need to configure the custom route so Clerk sends users to it during OAuth flows that require consent. You should also monitor Application Logs for OAuth grant activity. The oauth_authorization.granted event records successful user consent, and the oauth_token.created event records token issuance. Review these events for unfamiliar OAuth applications, unexpected users, or unusual grant volume.

Caution

Consent phishing is a real OAuth attack pattern. Attackers can trick users into granting a malicious app access to their account data without stealing their password. Microsoft describes consent phishing as an attack where users grant permissions to malicious cloud applications, and notes that MFA or password resets do not remediate illicit consent grants because the user authorized the app itself. See Microsoft's guidance on protecting against consent phishing and detecting illicit consent grants.

Before you start

Custom consent pages apply to Clerk's OAuth provider flows, where Clerk acts as the authorization service for OAuth applications that request access to user data through Clerk.

Note

If you're new to OAuth terminology, start with the OAuth terminology guide.

Before setting up the custom consent page:

  • Keep the consent screen enabled for every OAuth application. Without consent, a signed-in user who visits a valid authorization URL can grant access without seeing the request.
  • Use narrow scopes. OAuth security guidance recommends restricting access tokens to the minimum required privileges, and Google recommends choosing the most narrowly focused scopes possible. See RFC 9700 and Google's guide to configuring OAuth consent and choosing scopes.
  • Treat OAuth application branding as user-facing security information. App names, logos, client URLs, and redirect domains help users decide whether to grant access.
  • Verify the route in development and production before enabling it for real users. A broken custom consent page can block OAuth authorization flows.

A safe consent page must give users enough information to make an informed decision. At minimum, show:

  • The OAuth application's name.
  • The OAuth application's logo, if one is available.
  • The OAuth application's public URL, if one is available.
  • The receiving the access request.
  • The signed-in user who is granting access.
  • Every requested scope that requires consent, using clear descriptions.
  • The redirect destination the user will return to after allowing or denying access.
  • Equally visible Allow and Deny actions.

Show the redirect destination clearly

The redirect destination presentation is security-sensitive. The prebuilt consent page shows a short domain derived from the redirect_uri, and lets the user expand it to view the full URL. This helps prevent attackers from hiding the real root domain inside a very long URL with many subdomains or path segments. To preserve that safety:

  • If you build your own UI, do not show only a truncated full URL. Show a clear domain summary, provide a way to view the full redirect_uri, and make sure the root domain cannot be pushed out of view by long subdomains.
  • If you use the prebuilt component, do not hide the requested scopes, redirect warning, deny action, or application identity with appearance overrides.
  • If you build a custom flow, include equivalent warning copy near the allow/deny controls. For example: "<OAuth app name> will be able to access <application name> and redirect you to <redirect domain>. Review the requested permissions before continuing."

The safest way to customize the OAuth consent page is to host a custom route on your application domain and render Clerk's prebuilt <OAuthConsent /> component. The component reads the OAuth authorization parameters from the current URL, loads consent metadata for the signed-in user, renders the requested scopes, and submits the user's decision to Clerk.

The following example creates a route that renders <OAuthConsent />. When setting up this route:

  • Render the consent screen only for signed-in users using the <Show /> component. In a normal OAuth flow, Clerk redirects signed-out users to sign-in before sending them to the consent page. If users can visit the route directly, handle signed-out users the same way you would any other protected route.
  • Keep the route focused on the consent decision. Avoid shared navigation, account menus, sign-in or sign-out controls, or other links that could take the user away from the OAuth flow.
  • Set the referrer policy to strict-origin-when-cross-origin. Since the consent form posts to Clerk's , this referrer policy prevents some cross-origin form submissions from sending Origin: null, causing Clerk to reject the request. If your framework does not provide route metadata, set the same referrer policy with a <meta> tag.
src/routes/oauth-consent.tsx
import { OAuthConsent, Show } from '@clerk/tanstack-react-start'
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/oauth-consent')({
  head: () => ({
    meta: [
      {
        name: 'referrer',
        content: 'strict-origin-when-cross-origin',
      },
    ],
  }),
  component: OAuthConsentPage,
})

function OAuthConsentPage() {
  return (
    <Show when="signed-in">
      <OAuthConsent />
    </Show>
  )
}

Tip

You can style <OAuthConsent /> with the standard appearance prop while keeping Clerk's consent logic intact. This is the same approach Clerk uses for the Account Portal consent page, and it is the safer option when your goal is visual customization. For more guidance, see Use appearance for visual changes.

Build a custom flow

Building a from low-level APIs is riskier than using <OAuthConsent />. Only choose this path if you need a layout or interaction that the prebuilt component cannot support.

A custom flow must do the same work as the prebuilt component:

  1. Read client_id, redirect_uri, scope, state, nonce, code_challenge, code_challenge_method, and any other OAuth authorization parameters from the incoming URL.
  2. Load consent metadata for the signed-in user with useOAuthConsent() or Clerk.oauthApplication.getConsentInfo().
  3. Display the signed-in user, resource service, OAuth application name, logo URL, application URL, client ID, and scopes without changing their meaning.
  4. Display the redirect destination safely. Show a short domain summary, and let the user inspect the full redirect_uri.
  5. Preserve and forward the original OAuth authorization parameters when submitting the decision.
  6. Submit a form with method="POST" to the URL returned by Clerk.oauthApplication.buildConsentActionUrl({ clientId }).
  7. Use a submit field named consented, with value="true" for allow and value="false" for deny.
  8. Include organization_id when the user grants access for a specific organization.
  9. Preserve the user's ability to deny access. A denial returns an OAuth access_denied response to the OAuth client.

The following examples show the minimum shape of a custom consent page. They intentionally keep the UI plain so you can focus on the OAuth-specific requirements. These examples have a few important limitations:

  • The examples display the full redirect hostname and an expandable full URL. In production, use a public-suffix-aware approach for root-domain summaries, handle IP addresses and localhost explicitly, and test long redirect URIs to make sure the real destination remains visible.
  • These examples also do not implement organization selection. Until Clerk exposes a public organization selector for OAuth consent flows, if an OAuth application can request user:org:read, use <OAuthConsent /> or add an organization selector that submits the selected organization_id with the Allow action.

Important

Do not construct the by hand. Use buildConsentActionUrl() so Clerk can include the current session and development browser parameters that are required for the request.

src/routes/oauth-consent.tsx
import { Show, useClerk, useOAuthConsent, useUser } from '@clerk/tanstack-react-start'
import { createFileRoute, useLocation } from '@tanstack/react-router'

export const Route = createFileRoute('/oauth-consent')({
  head: () => ({
    meta: [
      {
        name: 'referrer',
        content: 'strict-origin-when-cross-origin',
      },
    ],
  }),
  component: OAuthConsentPage,
})

function CustomConsentForm() {
  const clerk = useClerk()
  const { user } = useUser()
  const location = useLocation()
  const searchParams = new URLSearchParams(location.searchStr)

  const clientId = searchParams.get('client_id') ?? ''
  const redirectUri = searchParams.get('redirect_uri') ?? ''
  const scope = searchParams.get('scope') ?? undefined

  const { data, isLoading, error } = useOAuthConsent({
    oauthClientId: clientId,
    scope,
    redirectUri,
  })

  if (!clientId || !redirectUri) {
    return <p>Missing OAuth consent parameters.</p>
  }

  if (isLoading) {
    return <p>Loading consent request...</p>
  }

  if (error || !data) {
    return <p>Unable to load consent request.</p>
  }

  return (
    <form method="POST" action={clerk.oauthApplication.buildConsentActionUrl({ clientId })}>
      <h1>{data.oauthApplicationName} wants access to your account</h1>

      {data.oauthApplicationLogoUrl && (
        <img src={data.oauthApplicationLogoUrl} alt={`${data.oauthApplicationName} logo`} />
      )}

      {data.oauthApplicationUrl && (
        <p>
          Application URL: <a href={data.oauthApplicationUrl}>{data.oauthApplicationUrl}</a>
        </p>
      )}
      <p>
        Client ID: <code>{data.clientId}</code>
      </p>
      <p>
        Signed-in user:{' '}
        <strong>{user?.primaryEmailAddress?.emailAddress ?? user?.username ?? user?.id}</strong>
      </p>
      <p>Resource service: Clerk user data</p>

      <p>
        Review the requested permissions before continuing. After you allow or deny access, you will
        be redirected to <strong>{data.redirectDomain || new URL(redirectUri).hostname}</strong>.
      </p>

      <details>
        <summary>View full redirect URL</summary>
        <code>{redirectUri}</code>
      </details>

      <ul>
        {data.scopes.map((scope) => (
          <li key={scope.scope}>{scope.description || scope.scope}</li>
        ))}
      </ul>

      {/* Forward the original OAuth parameters, except fields set by this form. */}
      {Array.from(searchParams.entries())
        .filter(([key]) => key !== 'consented' && key !== 'organization_id')
        .map(([key, value], index) => (
          <input key={`${key}:${index}`} type="hidden" name={key} value={value} />
        ))}

      <button type="submit" name="consented" value="false">
        Deny
      </button>
      <button type="submit" name="consented" value="true">
        Allow
      </button>
    </form>
  )
}

function OAuthConsentPage() {
  return (
    <Show when="signed-in">
      <CustomConsentForm />
    </Show>
  )
}

Configure the custom route

After you create and deploy your custom consent route, configure Clerk to send users to it during OAuth flows that require consent. This applies whether you use the prebuilt <OAuthConsent /> component or build a fully custom flow.

Open path settings

In the Clerk Dashboard, navigate to the Paths page of your application.

Under Component paths, locate the the OAuth consent section and choose your custom location:

  • For a development instance, enter a path on the development host, such as /oauth-consent.
  • For a production instance, enter an https:// URL on your application domain, such as https://example.com/oauth-consent. Clerk only accepts production OAuth consent URLs that use HTTPS and belong to the same registrable domain as your instance domain, such as example.com or a subdomain of example.com.

Navigate to OAuth applications and confirm that the consent screen is enabled for every OAuth application that can use the custom route.

If dynamic client registration is enabled, Clerk enforces the consent screen and does not allow it to be disabled.

Test an authorization request

Start an OAuth authorization flow for one of your OAuth applications. If the user is signed out, Clerk redirects to sign-in first, then redirects to your custom consent route with the original OAuth parameters. After the user allows or denies access, Clerk continues the OAuth flow and redirects back to the OAuth client's redirect_uri.

Use appearance for visual changes

If your main goal is to change colors, fonts, spacing, or logos, use the appearance prop with <OAuthConsent /> instead of building a custom flow. This keeps Clerk's consent behavior, redirect destination presentation, organization selection, form submission, and future security updates intact.

Only build a custom flow if:

  • The prebuilt component cannot support your required layout or interaction.
  • You can maintain the route as a security-sensitive surface.
  • You can show the requesting OAuth application's identity, requested scopes, and redirect destination clearly.
  • You can preserve an equally visible Deny action.

Do not build a custom flow to hide scopes, hide redirect destinations, remove the deny action, or auto-approve trusted clients. Instead, model trust through OAuth application configuration, narrow scopes, and administrative review. Custom consent pages should make the consent decision clearer, not easier to miss.

Security checklist

Before shipping a custom consent page, verify the following:

  • The page is served over HTTPS in production.
  • The page is on your application domain or subdomain, not on an unrelated domain.
  • The page cannot be framed by untrusted sites. Use appropriate Content-Security-Policy frame-ancestors or equivalent headers.
  • The page does not include third-party scripts that can read OAuth parameters or alter the consent form.
  • The page uses a minimal layout without unrelated navigation, account menus, or links that could take the user away from the OAuth flow.
  • The page displays the requesting OAuth application's identity and requested scopes before the user can allow access.
  • The page prevents code injection from OAuth application-provided values, including application names, logo URLs, application URLs, and redirect URLs.
  • The Deny action is visible and works.
  • The page never grants consent automatically.
  • The page forwards the original OAuth authorization parameters without allowing query parameters to override form-controlled fields such as consented or organization_id.
  • The page uses strict-origin-when-cross-origin referrer policy.
  • The OAuth application requests only the scopes it needs.
  • Your team can audit OAuth applications and revoke suspicious grants if needed.

Feedback

What did you think of this content?

Last updated on