import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' 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 { Admonition } from 'ui-patterns/admonition' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { PageSection, PageSectionContent } from 'ui-patterns/PageSection' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import * as z from 'zod' import { OAuthEndpointsTable } from './OAuthEndpointsTable' 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 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({ resolver: zodResolver(schema), 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 } if (isAuthConfigLoading || isLoadingPermissions) { return ( ) } return ( <>
( )} /> {form.watch('OAUTH_SERVER_ENABLED') && ( <> The base URL of your application, configured in{' '} Auth URL Configuration {' '} settings. } > ( )} /> {(() => { const siteUrl = authConfig?.SITE_URL?.trim() const authorizationPath = form.watch('OAUTH_SERVER_AUTHORIZATION_PATH')?.trim() || '/oauth/consent' const authorizationUrl = siteUrl ? `${siteUrl}${authorizationPath}` : '' return ( Preview Authorization URL:{' '} {authorizationUrl ? ( {authorizationUrl} ) : ( Set a Site URL to preview )} } /> ) })()} ( Enable dynamic OAuth app registration. Apps can be registered programmatically via APIs.{' '} Learn more } > )} /> )}
{isSuccess && authConfig?.OAUTH_SERVER_ENABLED && form.watch('OAUTH_SERVER_ENABLED') && ( )} {/* Dynamic Apps Confirmation Modal */}

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.

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.

Only enable this if you have a specific use case requiring programmatic client registration and understand the security implications.

{/* Disable OAuth Server Confirmation Modal */} 1 ? 's' : ''} that will be deactivated.`, }} >

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.

What will happen:

  • All OAuth apps will be deactivated
  • Existing access tokens will become invalid
  • Users won't be able to sign in through OAuth flows
  • Third-party integrations will stop working

You can re-enable the OAuth Server at any time.

) }