diff --git a/docs/_partials/hooks/hook-list.mdx b/docs/_partials/hooks/hook-list.mdx index 5d5164e1e3..0fb144a390 100644 --- a/docs/_partials/hooks/hook-list.mdx +++ b/docs/_partials/hooks/hook-list.mdx @@ -1,6 +1,7 @@ - [`useUser()`](/docs/reference/hooks/use-user) - [`useClerk()`](/docs/reference/hooks/use-clerk) - [`useAuth()`](/docs/reference/hooks/use-auth) +- [`useOAuthConsent()`](/docs/reference/hooks/use-oauth-consent) - [`useSignIn()`](/docs/reference/hooks/use-sign-in) - [`useSignUp()`](/docs/reference/hooks/use-sign-up) - [`useWaitlist()`](/docs/reference/hooks/use-waitlist) diff --git a/docs/_partials/oauth-consent/callout.mdx b/docs/_partials/oauth-consent/callout.mdx new file mode 100644 index 0000000000..6efaedd119 --- /dev/null +++ b/docs/_partials/oauth-consent/callout.mdx @@ -0,0 +1,2 @@ +> [!IMPORTANT] +> Pages that host OAuth consent flows must set the referrer policy to `strict-origin-when-cross-origin`. This ensures the cross-origin `POST` request to FAPI includes the `Origin` header and Clerk can validate the CSRF token. diff --git a/docs/manifest.json b/docs/manifest.json index a190172709..2a122ddb21 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -2309,6 +2309,10 @@ "title": "`useAuth()`", "href": "/docs/reference/hooks/use-auth" }, + { + "title": "`useOAuthConsent()`", + "href": "/docs/reference/hooks/use-oauth-consent" + }, { "title": "`useSignIn()`", "href": "/docs/reference/hooks/use-sign-in" @@ -2611,6 +2615,18 @@ "title": "Metadata types", "href": "/docs/reference/types/metadata" }, + { + "title": "`OAuthApplication`", + "href": "/docs/reference/types/oauth-application" + }, + { + "title": "`OAuthConsentInfo`", + "href": "/docs/reference/types/oauth-consent-info" + }, + { + "title": "`OAuthConsentScope`", + "href": "/docs/reference/types/oauth-consent-scope" + }, { "title": "`OrganizationCreationDefaults`", "href": "/docs/reference/types/organization-creation-defaults" @@ -3627,6 +3643,10 @@ "title": "``", "href": "/docs/reference/components/authentication/google-one-tap" }, + { + "title": "``", + "href": "/docs/reference/components/authentication/oauth-consent" + }, { "title": "``", "href": "/docs/reference/components/authentication/task-choose-organization" diff --git a/docs/reference/components/authentication/oauth-consent.mdx b/docs/reference/components/authentication/oauth-consent.mdx new file mode 100644 index 0000000000..6741b2edd4 --- /dev/null +++ b/docs/reference/components/authentication/oauth-consent.mdx @@ -0,0 +1,281 @@ +--- +title: '`` component' +description: Clerk's component renders an OAuth consent screen for authenticated users. +sdk: astro, nextjs, nuxt, react, react-router, tanstack-react-start, vue, js-frontend +--- + +The `` component renders an OAuth consent screen for authenticated users after they are redirected to the OAuth authorization page. It retrieves the OAuth application's consent metadata and displays the requested scopes, allowing the user to approve or deny access. + +By default, `` reads the OAuth `client_id`, `scope`, and `redirect_uri` from the current URL. You can override the `client_id` and `scope` with props when rendering the component on a custom route. + +> [!IMPORTANT] +> `` only renders for authenticated users. If the user is signed out, the component will not be displayed. + + + +## Example + +The following example includes a basic implementation of the `` component. You can use this as a starting point for your own implementation. + + + ```tsx {{ filename: 'app/oauth-consent/page.tsx' }} + import { OAuthConsent } from '@clerk/nextjs' + + export const metadata: Metadata = { + referrer: 'strict-origin-when-cross-origin', + } + + export default function Page() { + return + } + ``` + + + + ```tsx {{ filename: 'src/routes/oauth-consent.tsx' }} + import { OAuthConsent } from '@clerk/react' + + export const meta = () => [ + { + name: 'referrer', + content: 'strict-origin-when-cross-origin', + }, + ] + + export default function OAuthConsentPage() { + return + } + ``` + + + + ```tsx {{ filename: 'app/routes/oauth-consent.tsx' }} + import type { Route } from './+types/oauth-consent' + import { OAuthConsent } from '@clerk/react-router' + + export const meta: Route.MetaFunction = () => [ + { + name: 'referrer', + content: 'strict-origin-when-cross-origin', + }, + ] + + export default function OAuthConsentPage() { + return + } + ``` + + + + ```tsx {{ filename: 'app/routes/oauth-consent.tsx' }} + import { OAuthConsent } 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 + } + ``` + + + + ```astro {{ filename: 'pages/oauth-consent.astro' }} + --- + import { OAuthConsent } from '@clerk/astro/components' + --- + + + + + + + + + + ``` + + + + ```vue {{ filename: 'oauth-consent.vue' }} + + + + ``` + + + + ```vue {{ filename: 'oauth-consent.vue' }} + + + + ``` + + + + ## Usage with JavaScript + + Add the following `` tag to the page that hosts the OAuth consent screen: + + ```html {{ filename: 'index.html' }} + + ``` + + The following methods available on an instance of the [`Clerk`](/docs/reference/objects/clerk) class are used to render and control the `` component: + + - [`mountOAuthConsent()`](#mount-o-auth-consent) + - [`unmountOAuthConsent()`](#unmount-o-auth-consent) + + The following examples assume that you have followed the [quickstart](/docs/js-frontend/getting-started/quickstart) in order to add Clerk to your JavaScript application. + + ### `mountOAuthConsent()` + + Render the `` component to an HTML `
` element. + + ```typescript + function mountOAuthConsent(node: HTMLDivElement, props?: OAuthConsentProps): void + ``` + + #### `mountOAuthConsent()` params + + + - `node` + - [`HTMLDivElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLDivElement) + + The container `
` element used to render the `` component. + + --- + + - `props?` + - [`OAuthConsentProps`](#properties) + + The properties to pass to the `` component. + + + #### `mountOAuthConsent()` usage + + ```js {{ filename: 'main.js', mark: [15] }} + import { Clerk } from '@clerk/clerk-js' + + const clerkPubKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY + + const clerk = new Clerk(clerkPubKey) + await clerk.load() + + document.getElementById('app').innerHTML = ` + + ` + + const oauthConsentDiv = document.getElementById('oauth-consent') + + clerk.mountOAuthConsent(oauthConsentDiv) + ``` + + ### `unmountOAuthConsent()` + + Unmount and run cleanup on an existing `` component instance. + + ```typescript + function unmountOAuthConsent(node: HTMLDivElement): void + ``` + + #### `unmountOAuthConsent()` params + + + - `node` + - [`HTMLDivElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLDivElement) + + The container `
` element with a rendered `` component instance. + + + #### `unmountOAuthConsent()` usage + + ```js {{ filename: 'main.js', mark: [19] }} + import { Clerk } from '@clerk/clerk-js' + + const clerkPubKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY + + const clerk = new Clerk(clerkPubKey) + await clerk.load() + + document.getElementById('app').innerHTML = ` + + ` + + const oauthConsentDiv = document.getElementById('oauth-consent') + + clerk.mountOAuthConsent(oauthConsentDiv) + + // ... + + clerk.unmountOAuthConsent(oauthConsentDiv) + ``` + + +## Properties + +All props are optional. + + + - `appearance?` + - [Appearance](/docs/guides/customizing-clerk/appearance-prop/overview) | undefined + + An object to style your components. Will only affect [Clerk components](/docs/reference/components/overview) and not [Account Portal](/docs/guides/account-portal/overview) pages. + + --- + + - `fallback?` + - `ReactNode` + + An element to be rendered while the component is mounting. + + --- + + - `oauthClientId?` + - `string` + + Override the OAuth client ID. By default, Clerk reads this from the `client_id` query parameter in the current URL. + + --- + + - `scope?` + - `string` + + Override the OAuth scope. By default, Clerk reads this from the `scope` query parameter in the current URL. + diff --git a/docs/reference/components/overview.mdx b/docs/reference/components/overview.mdx index 28235d2994..0f8d2e8a5d 100644 --- a/docs/reference/components/overview.mdx +++ b/docs/reference/components/overview.mdx @@ -11,6 +11,7 @@ Clerk offers a comprehensive suite of components designed to seamlessly integrat - [``](/docs/reference/components/authentication/sign-in) - [``](/docs/reference/components/authentication/sign-up) - [``](/docs/reference/components/authentication/google-one-tap) +- [``](/docs/reference/components/authentication/oauth-consent) - [``](/docs/reference/components/authentication/task-choose-organization) - [``](/docs/reference/components/authentication/task-reset-password) - [``](/docs/reference/components/authentication/task-setup-mfa) diff --git a/docs/reference/hooks/use-oauth-consent.mdx b/docs/reference/hooks/use-oauth-consent.mdx new file mode 100644 index 0000000000..b5d24e8deb --- /dev/null +++ b/docs/reference/hooks/use-oauth-consent.mdx @@ -0,0 +1,245 @@ +--- +title: useOAuthConsent() +description: Load OAuth consent metadata for an authenticated user with Clerk's useOAuthConsent() hook. +sdk: nextjs, react, react-router, tanstack-react-start +--- + +The `useOAuthConsent()` hook loads OAuth consent metadata for an authenticated user. You can use it to build a custom OAuth consent page that fetches the OAuth application's name, logo, URL, requested scopes, and related loading state. + +Unlike the [``](/docs/reference/components/authentication/oauth-consent) component, this hook doesn't read values from the current URL. Pass the `oauthClientId` explicitly, and optionally pass `scope` if you want Clerk to scope the request to a specific space-delimited scope string. + +## Parameters + +`useOAuthConsent()` accepts a single object with the following properties: + + + +## Returns + + + + + +## Example + +The following example demonstrates how to use the `useOAuthConsent()` hook to fetch OAuth consent metadata and build a custom consent form. In this example, the `client_id` is read from the current URL and passed to the hook, and the form submits to the URL generated by the [`buildConsentActionUrl()`](/docs/reference/types/oauth-application) method. + + + ```tsx {{ filename: 'src/routes/oauth-consent.tsx' }} + import { useClerk, useOAuthConsent } from '@clerk/react' + + export const meta = () => [ + { + name: 'referrer', + content: 'strict-origin-when-cross-origin', + }, + ] + + export default function OAuthConsentPage() { + const clerk = useClerk() + const params = new URLSearchParams(window.location.search) + const clientId = params.get('client_id') ?? '' + const scope = params.get('scope') ?? undefined + + const { data, isLoading, error } = useOAuthConsent({ + oauthClientId: clientId, + scope, + }) + + const actionUrl = clerk.oauthApplication.buildConsentActionUrl({ clientId }) + + if (isLoading) return
Loading...
+ if (error) return
Something went wrong.
+ + return ( +
+

{data.oauthApplicationName} wants access to your account

+
    + {data.scopes.map((scope) => ( +
  • {scope.description || scope.scope}
  • + ))} +
+ + + + + {Array.from(params.entries()).map(([key, value]) => ( + + ))} +
+ ) + } + ``` +
+ + + ```tsx {{ filename: 'app/oauth-consent/page.tsx' }} + 'use client' + + import { useClerk, useOAuthConsent } from '@clerk/nextjs' + import { useSearchParams } from 'next/navigation' + + export const metadata: Metadata = { + referrer: 'strict-origin-when-cross-origin', + } + + export default function Page() { + const clerk = useClerk() + const params = useSearchParams() + const clientId = params.get('client_id') ?? '' + const scope = params.get('scope') ?? undefined + + const { data, isLoading, error } = useOAuthConsent({ + oauthClientId: clientId, + scope, + }) + + const actionUrl = clerk.oauthApplication.buildConsentActionUrl({ clientId }) + + if (isLoading) return
Loading...
+ if (error) return
Something went wrong.
+ + return ( +
+

{data.oauthApplicationName} wants access to your account

+
    + {data.scopes.map((scope) => ( +
  • {scope.description ?? scope.scope}
  • + ))} +
+ + + + + {Array.from(params.entries()).map(([key, value]) => ( + + ))} +
+ ) + } + ``` +
+ + + ```tsx {{ filename: 'app/routes/oauth-consent.tsx' }} + import type { Route } from './+types/oauth-consent' + import { useClerk, useOAuthConsent } from '@clerk/react-router' + import { useSearchParams } from 'react-router' + + export const meta: Route.MetaFunction = () => [ + { + name: 'referrer', + content: 'strict-origin-when-cross-origin', + }, + ] + + export default function OAuthConsentPage() { + const clerk = useClerk() + const [params] = useSearchParams() + const clientId = params.get('client_id') ?? '' + const scope = params.get('scope') ?? undefined + + const { data, isLoading, error } = useOAuthConsent({ + oauthClientId: clientId, + scope, + }) + + const actionUrl = clerk.oauthApplication.buildConsentActionUrl({ clientId }) + + if (isLoading) return
Loading...
+ if (error) return
Something went wrong.
+ + return ( +
+

{data.oauthApplicationName} wants access to your account

+
    + {data.scopes.map((scope) => ( +
  • {scope.description || scope.scope}
  • + ))} +
+ + + + + {Array.from(params.entries()).map(([key, value]) => ( + + ))} +
+ ) + } + ``` +
+ + + ```tsx {{ filename: 'app/routes/oauth-consent.tsx' }} + import { useClerk, useOAuthConsent } 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', + }, + ], + }), + validateSearch: (search: Record) => ({ + client_id: typeof search.client_id === 'string' ? search.client_id : '', + scope: typeof search.scope === 'string' ? search.scope : undefined, + }), + component: OAuthConsentPage, + }) + + function OAuthConsentPage() { + const clerk = useClerk() + const params = Route.useSearch() + const clientId = params.client_id + const scope = params.scope + + const { data, isLoading, error } = useOAuthConsent({ + oauthClientId: clientId, + scope, + }) + + const actionUrl = clerk.oauthApplication.buildConsentActionUrl({ clientId }) + + if (isLoading) return
Loading...
+ if (error) return
Something went wrong.
+ + return ( +
+

{data.oauthApplicationName} wants access to your account

+
    + {data.scopes.map((scope) => ( +
  • {scope.description || scope.scope}
  • + ))} +
+ + + + + {Object.entries(params).map(([key, value]) => ( + + ))} +
+ ) + } + ``` +
diff --git a/docs/reference/objects/clerk.mdx b/docs/reference/objects/clerk.mdx index 8bf3f82f75..d8889848a3 100644 --- a/docs/reference/objects/clerk.mdx +++ b/docs/reference/objects/clerk.mdx @@ -115,6 +115,13 @@ The `Clerk` class serves as the central interface for working with Clerk's authe --- + - `oauthApplication` + - [`OAuthApplication`][oauth-application-ref] + + The `OAuthApplication` object used for building custom OAuth consent flows. + + --- + - `proxyUrl` - `string | undefined` @@ -1433,6 +1440,11 @@ The `Clerk` class also contains a number of methods for interacting with prebuil - [`mountOrganizationList`](/docs/reference/components/organization/organization-list#mount-organization-list) - [`unmountOrganizationList`](/docs/reference/components/organization/organization-list#unmount-organization-list) +### `` + +- [`mountOAuthConsent`](/docs/reference/components/authentication/oauth-consent#mount-o-auth-consent) +- [`unmountOAuthConsent`](/docs/reference/components/authentication/oauth-consent#unmount-o-auth-consent) + ### `` - [`mountWaitlist()`](/docs/reference/components/authentication/waitlist#mount-waitlist) @@ -1476,6 +1488,8 @@ The `Clerk` class also contains a number of methods for interacting with prebuil [organization-ref]: /docs/reference/objects/organization +[oauth-application-ref]: /docs/reference/types/oauth-application + [api-ref]: /docs/reference/objects/api-keys [billing-ref]: /docs/reference/objects/billing diff --git a/docs/reference/types/oauth-application.mdx b/docs/reference/types/oauth-application.mdx new file mode 100644 index 0000000000..1c8dbaaadd --- /dev/null +++ b/docs/reference/types/oauth-application.mdx @@ -0,0 +1,71 @@ +--- +title: '`OAuthApplication`' +description: The OAuthApplication object provides helpers for building custom OAuth consent flows in Clerk. +sdk: js-frontend, astro, chrome-extension, expo, nextjs, react, react-router, tanstack-react-start, nuxt, vue +--- + +The `OAuthApplication` object provides helpers for building custom OAuth consent flows in Clerk. + +Use this object together with the [`useOAuthConsent()`](/docs/reference/hooks/use-oauth-consent) hook or the [``](/docs/reference/components/authentication/oauth-consent) component when you need to render a custom consent page for an OAuth application. + +## Methods + +### `buildConsentActionUrl()` + +Returns the URL to use as the `action` attribute of your consent form. Clerk includes the current session context automatically, including `_clerk_session_id` and, in development, the dev browser JWT. + +```typescript +function buildConsentActionUrl(params: { clientId: string }): string +``` + +#### Parameters + + + - `clientId` + - `string` + + The OAuth `client_id` from the authorize request. + + +#### Example + +```tsx +const actionUrl = clerk.oauthApplication.buildConsentActionUrl({ + clientId, +}) +``` + +### `getConsentInfo()` + +Loads the consent metadata for a signed-in user and OAuth application. Returns a [`OAuthConsentInfo`](/docs/reference/types/oauth-consent-info) object. + +Use this method when you want to build your own consent UI without the [`useOAuthConsent()`](/docs/reference/hooks/use-oauth-consent) hook. + +```typescript +function getConsentInfo(params: GetOAuthConsentInfoParams): Promise +``` + +#### `GetOAuthConsentInfoParams` + + + - `oauthClientId` + - `string` + + The OAuth `client_id` from the authorize request. + + --- + + - `scope?` + - `string` + + A space-delimited scope string from the authorize request. + + +#### Example + +```tsx +const consentInfo = await clerk.oauthApplication.getConsentInfo({ + oauthClientId: clientId, + scope, +}) +``` diff --git a/docs/reference/types/oauth-consent-info.mdx b/docs/reference/types/oauth-consent-info.mdx new file mode 100644 index 0000000000..31cb85498a --- /dev/null +++ b/docs/reference/types/oauth-consent-info.mdx @@ -0,0 +1,7 @@ +--- +title: '`OAuthConsentInfo`' +description: An interface representing OAuth consent information. +sdk: js-frontend, astro, chrome-extension, expo, nextjs, react, react-router, tanstack-react-start, nuxt, vue +--- + + diff --git a/docs/reference/types/oauth-consent-scope.mdx b/docs/reference/types/oauth-consent-scope.mdx new file mode 100644 index 0000000000..b1c313f1f2 --- /dev/null +++ b/docs/reference/types/oauth-consent-scope.mdx @@ -0,0 +1,7 @@ +--- +title: '`OAuthConsentScope`' +description: An interface representing a single OAuth scope with its description and whether it requires consent. +sdk: js-frontend, astro, chrome-extension, expo, nextjs, react, react-router, tanstack-react-start, nuxt, vue +--- + + diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index d695ded7b0..7bdafc126c 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -197,11 +197,7 @@ async function main() { 'guides/development/webhooks/inngest.mdx': ['doc-not-in-manifest'], 'guides/development/webhooks/loops.mdx': ['doc-not-in-manifest'], }, - typedoc: { - 'shared/o-auth-application-namespace.mdx': ['link-doc-not-found'], - 'shared/o-auth-consent-info.mdx': ['link-doc-not-found'], - 'shared/use-o-auth-consent-return.mdx': ['link-doc-not-found'], - }, + typedoc: {}, partials: {}, tooltips: {}, },