| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435 |
- import { zodResolver } from '@hookform/resolvers/zod'
- import { PermissionAction } from '@supabase/shared-types/out/constants'
- import { useParams } from 'common'
- import dynamic from 'next/dynamic'
- import Link from 'next/link'
- import { useEffect, useState } from 'react'
- import { useForm } from 'react-hook-form'
- import { toast } from 'sonner'
- import {
- Button,
- Card,
- CardContent,
- CardFooter,
- Form,
- FormControl,
- FormField,
- Input,
- Switch,
- } from 'ui'
- import { PageSection, PageSectionContent } from 'ui-patterns'
- import { Admonition } from 'ui-patterns/admonition'
- import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
- import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
- import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
- import * as z from 'zod'
- import { InlineLink } from '@/components/ui/InlineLink'
- import NoPermission from '@/components/ui/NoPermission'
- import { useAuthConfigQuery } from '@/data/auth/auth-config-query'
- import { useAuthConfigUpdateMutation } from '@/data/auth/auth-config-update-mutation'
- import { useOAuthServerAppsQuery } from '@/data/oauth-server-apps/oauth-server-apps-query'
- import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
- import { DOCS_URL } from '@/lib/constants'
- const OAuthEndpointsTable = dynamic(() =>
- import('./OAuthEndpointsTable').then((mod) => ({ default: mod.OAuthEndpointsTable }))
- )
- const configUrlSchema = z.object({
- id: z.string(),
- name: z.string(),
- value: z.string(),
- description: z.string().optional(),
- })
- const schema = z
- .object({
- OAUTH_SERVER_ENABLED: z.boolean().default(false),
- OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION: z.boolean().default(false),
- OAUTH_SERVER_AUTHORIZATION_PATH: z.string().default(''),
- availableScopes: z.array(z.string()).default(['openid', 'email', 'profile']),
- config_urls: z.array(configUrlSchema).optional(),
- })
- .superRefine((data, ctx) => {
- if (data.OAUTH_SERVER_ENABLED && data.OAUTH_SERVER_AUTHORIZATION_PATH.trim() === '') {
- ctx.addIssue({
- path: ['OAUTH_SERVER_AUTHORIZATION_PATH'],
- code: z.ZodIssueCode.custom,
- message: 'Authorization Path is required when OAuth Server is enabled.',
- })
- }
- })
- interface ConfigUrl {
- id: string
- name: string
- value: string
- description?: string
- }
- interface OAuthServerSettings {
- OAUTH_SERVER_ENABLED: boolean
- OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION: boolean
- OAUTH_SERVER_AUTHORIZATION_PATH?: string
- availableScopes: string[]
- config_urls?: ConfigUrl[]
- }
- export const OAuthServerSettingsForm = () => {
- const { ref: projectRef } = useParams()
- const {
- data: authConfig,
- isPending: isAuthConfigLoading,
- isSuccess,
- } = useAuthConfigQuery({ projectRef })
- const { mutate: updateAuthConfig, isPending } = useAuthConfigUpdateMutation({
- onSuccess: (_, variables) => {
- toast.success('OAuth server settings updated successfully')
- form.reset({
- OAUTH_SERVER_ENABLED: variables.config.OAUTH_SERVER_ENABLED ?? false,
- OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION:
- variables.config.OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION ?? false,
- OAUTH_SERVER_AUTHORIZATION_PATH:
- variables.config.OAUTH_SERVER_AUTHORIZATION_PATH ?? '/oauth/consent',
- availableScopes: ['openid', 'email', 'profile'],
- })
- },
- onError: (error) => {
- toast.error(`Failed to update OAuth server settings: ${error?.message}`)
- },
- })
- const [showDynamicAppsConfirmation, setShowDynamicAppsConfirmation] = useState(false)
- const [showDisableOAuthServerConfirmation, setShowDisableOAuthServerConfirmation] =
- useState(false)
- const {
- can: canReadConfig,
- isLoading: isLoadingPermissions,
- isSuccess: isPermissionsLoaded,
- } = useAsyncCheckPermissions(PermissionAction.READ, 'custom_config_gotrue')
- const { data: oAuthAppsData } = useOAuthServerAppsQuery({ projectRef })
- const oauthApps = oAuthAppsData?.clients || []
- const { can: canUpdateConfig } = useAsyncCheckPermissions(
- PermissionAction.UPDATE,
- 'custom_config_gotrue'
- )
- const form = useForm<OAuthServerSettings>({
- resolver: zodResolver(schema as any),
- defaultValues: {
- OAUTH_SERVER_ENABLED: true,
- OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION: false,
- OAUTH_SERVER_AUTHORIZATION_PATH: '/oauth/consent',
- availableScopes: ['openid', 'email', 'profile'],
- },
- })
- // Reset the values when the authConfig is loaded
- useEffect(() => {
- if (isSuccess && authConfig) {
- form.reset({
- OAUTH_SERVER_ENABLED: authConfig.OAUTH_SERVER_ENABLED ?? false,
- OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION:
- authConfig.OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION ?? false,
- OAUTH_SERVER_AUTHORIZATION_PATH:
- authConfig.OAUTH_SERVER_AUTHORIZATION_PATH ?? '/oauth/consent',
- availableScopes: ['openid', 'email', 'profile'], // Keep default scopes
- })
- }
- }, [isSuccess])
- const onSubmit = async (values: OAuthServerSettings) => {
- if (!projectRef) return console.error('Project ref is required')
- const config = {
- OAUTH_SERVER_ENABLED: values.OAUTH_SERVER_ENABLED,
- OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION: values.OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION,
- OAUTH_SERVER_AUTHORIZATION_PATH: values.OAUTH_SERVER_AUTHORIZATION_PATH,
- }
- updateAuthConfig({ projectRef, config })
- }
- const handleDynamicAppsToggle = (checked: boolean) => {
- if (checked) {
- setShowDynamicAppsConfirmation(true)
- } else {
- form.setValue('OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION', false, { shouldDirty: true })
- }
- }
- const confirmDynamicApps = () => {
- form.setValue('OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION', true, { shouldDirty: true })
- setShowDynamicAppsConfirmation(false)
- }
- const cancelDynamicApps = () => {
- setShowDynamicAppsConfirmation(false)
- }
- const handleOAuthServerToggle = (checked: boolean) => {
- if (!checked && oauthApps.length > 0) {
- setShowDisableOAuthServerConfirmation(true)
- } else {
- form.setValue('OAUTH_SERVER_ENABLED', checked, { shouldDirty: true })
- }
- }
- const confirmDisableOAuthServer = () => {
- form.setValue('OAUTH_SERVER_ENABLED', false, { shouldDirty: true })
- setShowDisableOAuthServerConfirmation(false)
- }
- const cancelDisableOAuthServer = () => {
- setShowDisableOAuthServerConfirmation(false)
- }
- if (isPermissionsLoaded && !canReadConfig) {
- return <NoPermission resourceText="view OAuth server settings" />
- }
- if (isAuthConfigLoading || isLoadingPermissions) {
- return (
- <PageSection>
- <PageSectionContent>
- <Card>
- <CardContent>
- <GenericSkeletonLoader />
- </CardContent>
- </Card>
- <OAuthEndpointsTable isLoading />
- </PageSectionContent>
- </PageSection>
- )
- }
- return (
- <>
- <PageSection>
- <PageSectionContent>
- <Form {...form}>
- <form onSubmit={form.handleSubmit(onSubmit)}>
- <Card>
- <CardContent>
- <FormField
- control={form.control}
- name="OAUTH_SERVER_ENABLED"
- render={({ field }) => (
- <FormItemLayout
- layout="flex-row-reverse"
- label="Enable the Briven OAuth Server"
- description="Enable OAuth server functionality for your project to create and manage OAuth applications."
- >
- <FormControl>
- <Switch
- checked={field.value}
- onCheckedChange={handleOAuthServerToggle}
- disabled={!canUpdateConfig}
- />
- </FormControl>
- </FormItemLayout>
- )}
- />
- </CardContent>
- {form.watch('OAUTH_SERVER_ENABLED') && (
- <>
- <CardContent>
- <FormItemLayout
- label="Site URL"
- layout="flex-row-reverse"
- description={
- <>
- The base URL of your application, configured in{' '}
- <Link
- href={`/project/${projectRef}/auth/url-configuration`}
- rel="noreferrer"
- className="text-foreground-light underline hover:text-foreground transition"
- >
- Auth URL Configuration
- </Link>{' '}
- settings.
- </>
- }
- >
- <Input
- value={authConfig?.SITE_URL}
- disabled
- placeholder="https://example.com"
- />
- </FormItemLayout>
- </CardContent>
- <CardContent className="space-y-4">
- <FormField
- control={form.control}
- name="OAUTH_SERVER_AUTHORIZATION_PATH"
- render={({ field }) => (
- <FormItemLayout
- label="Authorization Path"
- layout="flex-row-reverse"
- description="Path where you'll implement the OAuth authorization UI (consent screens)."
- >
- <FormControl>
- <Input {...field} placeholder="/auth/authorize" />
- </FormControl>
- </FormItemLayout>
- )}
- />
- {(() => {
- const siteUrl = authConfig?.SITE_URL?.trim()
- const authorizationPath =
- form.watch('OAUTH_SERVER_AUTHORIZATION_PATH')?.trim() || '/oauth/consent'
- const authorizationUrl = siteUrl ? `${siteUrl}${authorizationPath}` : ''
- return (
- <Admonition
- type="tip"
- title="Make sure this path is implemented in your application."
- description={
- <>
- Preview Authorization URL:{' '}
- {authorizationUrl ? (
- <a
- href={authorizationUrl}
- target="_blank"
- rel="noreferrer"
- className="text-foreground-light underline hover:text-foreground transition"
- >
- {authorizationUrl}
- </a>
- ) : (
- <span className="text-foreground-light">
- Set a Site URL to preview
- </span>
- )}
- </>
- }
- />
- )
- })()}
- </CardContent>
- <CardContent>
- <FormField
- control={form.control}
- name="OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION"
- render={({ field }) => (
- <FormItemLayout
- layout="flex-row-reverse"
- label="Allow Dynamic OAuth Apps"
- description={
- <>
- Enable dynamic OAuth app registration. Apps can be registered
- programmatically via APIs.{' '}
- <InlineLink
- href={`${DOCS_URL}/guides/auth/oauth-server/mcp-authentication#oauth-client-setup`}
- >
- Learn more
- </InlineLink>
- </>
- }
- >
- <FormControl>
- <Switch
- checked={field.value}
- onCheckedChange={handleDynamicAppsToggle}
- disabled={!canUpdateConfig}
- />
- </FormControl>
- </FormItemLayout>
- )}
- />
- </CardContent>
- </>
- )}
- <CardFooter className="justify-end space-x-2">
- <Button type="default" onClick={() => form.reset()} disabled={isPending}>
- Cancel
- </Button>
- <Button
- type="primary"
- htmlType="submit"
- disabled={!canUpdateConfig || !form.formState.isDirty}
- loading={isPending}
- >
- Save changes
- </Button>
- </CardFooter>
- </Card>
- </form>
- </Form>
- </PageSectionContent>
- </PageSection>
- {isSuccess && authConfig?.OAUTH_SERVER_ENABLED && form.watch('OAUTH_SERVER_ENABLED') && (
- <OAuthEndpointsTable isLoading={isPending} />
- )}
- {/* Dynamic Apps Confirmation Modal */}
- <ConfirmationModal
- variant="warning"
- visible={showDynamicAppsConfirmation}
- size="large"
- title="Enable dynamic OAuth app registration"
- confirmLabel="Enable dynamic app registration"
- onConfirm={confirmDynamicApps}
- onCancel={cancelDynamicApps}
- alert={{
- title:
- 'By confirming, you acknowledge the risks and would like to move forward with enabling dynamic OAuth app registration.',
- }}
- >
- <p className="text-sm text-foreground-lighter pb-4">
- Dynamic OAuth apps (also known as dynamic client registration) exposes a public endpoint
- allowing anyone to register OAuth clients. Bad actors could create malicious apps with
- legitimate-sounding names to phish your users for authorization.
- </p>
- <p className="text-sm text-foreground-lighter pb-4">
- You may also see spam registrations that are difficult to trace or moderate, making it
- harder to identify trustworthy applications in your OAuth apps list.
- </p>
- <p className="text-sm text-foreground-lighter pb-4">
- Only enable this if you have a specific use case requiring programmatic client
- registration and understand the security implications.
- </p>
- </ConfirmationModal>
- {/* Disable OAuth Server Confirmation Modal */}
- <ConfirmationModal
- variant="warning"
- visible={showDisableOAuthServerConfirmation}
- size="large"
- title="Disable OAuth Server"
- confirmLabel="Disable OAuth Server"
- onConfirm={confirmDisableOAuthServer}
- onCancel={cancelDisableOAuthServer}
- alert={{
- title: `You have ${oauthApps.length} active OAuth app${oauthApps.length > 1 ? 's' : ''} that will be deactivated.`,
- }}
- >
- <p className="text-sm text-foreground-lighter pb-4">
- Disabling the OAuth Server will immediately deactivate all OAuth applications and prevent
- new authentication flows from working. This action will affect all users currently using
- your OAuth applications.
- </p>
- <p className="text-sm text-foreground-lighter pb-4">
- <strong>What will happen:</strong>
- </p>
- <ul className="text-sm text-foreground-lighter pb-4 list-disc list-inside space-y-1">
- <li>All OAuth apps will be deactivated</li>
- <li>Existing access tokens will become invalid</li>
- <li>Users won't be able to sign in through OAuth flows</li>
- <li>Third-party integrations will stop working</li>
- </ul>
- <p className="text-sm text-foreground-lighter pb-4">
- You can re-enable the OAuth Server at any time.
- </p>
- </ConfirmationModal>
- </>
- )
- }
|