chore(studio): show oauth server endpoints in oauth server settings (#41783)

Show OAuth server endpoints in oauth server settings page.

Preview: [OAuth Server
settings](https://studio-staging-git-chore-show-oauth-server-endpoints-supabase.vercel.app/dashboard/project/_/auth/oauth-server)

<img width="1138" height="496" alt="Screenshot 2026-01-09 at 12 00 31"
src="https://github.com/user-attachments/assets/eeca7726-0426-4abe-990d-271b702e4f7b"
/>

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added an OAuth endpoints table showing Authorization, Token, JWKS, and
Discovery/OpenID URLs with copy-to-clipboard and a masked preview mode.
* Inline preview of the Authorization URL when an authorization path is
set.

* **Improvements**
* Reorganized OAuth server settings for clearer enable/disable flow,
conditional field visibility, and disable confirmation.
* Dynamic loading of the endpoints table, improved loading skeletons,
layout refinements, and form reset to reflect saved defaults.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Ali Waseem <waseema393@gmail.com>
This commit is contained in:
Francesco SansalvadoreandAli Waseem authored and GitHub committed 2026-04-09 16:01:26 +02:00
1 parent a325e86845
commit 82f798f75a
5 files changed
+350 -138

No files matched your search

@@ -0,0 +1,90 @@
import { useParams } from 'common'
import { Card, CardContent, cn } from 'ui'
import {
PageSection,
PageSectionContent,
PageSectionDescription,
PageSectionMeta,
PageSectionSummary,
PageSectionTitle,
} from 'ui-patterns'
import { Input } from 'ui-patterns/DataInputs/Input'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { useOpenIDConfigurationQuery } from '@/data/oauth-server-apps/oauth-openid-configuration-query'
interface OAuthEndpointsTableProps {
isLoading?: boolean
className?: string
}
export const OAuthEndpointsTable = ({
isLoading: isLoadingProp = false,
className,
}: OAuthEndpointsTableProps) => {
const { ref: projectRef } = useParams()
const { data: openidConfig, isLoading: isEndpointsLoading } = useOpenIDConfigurationQuery(
{ projectRef },
{ enabled: !isLoadingProp }
)
const isLoading = isLoadingProp || isEndpointsLoading
const endpoints = [
{
name: 'Authorization endpoint',
value: openidConfig?.authorization_endpoint,
},
{
name: 'Token endpoint',
value: openidConfig?.token_endpoint,
},
{
name: 'JWKS endpoint',
value: openidConfig?.jwks_uri,
},
{
name: 'OIDC discovery',
value: openidConfig?.issuer
? `${openidConfig.issuer}/.well-known/openid-configuration`
: undefined,
},
]
return (
<PageSection className={cn(className)}>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>OAuth Endpoints</PageSectionTitle>
<PageSectionDescription>
Share these endpoints with third-party applications that need to integrate with your
OAuth 2.1 server.
</PageSectionDescription>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Card>
<CardContent className="flex flex-col gap-4 pt-0 divide-y">
{isLoading ? (
<GenericSkeletonLoader className="mt-4" />
) : (
endpoints.map((endpoint) => (
<FormItemLayout
key={endpoint.name}
layout="horizontal"
isReactForm={false}
label={endpoint.name}
className="mt-4"
>
<Input readOnly copy value={endpoint.value ?? ''} />
</FormItemLayout>
))
)}
</CardContent>
</Card>
</PageSectionContent>
</PageSection>
)
}
@@ -1,6 +1,7 @@
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'
@@ -16,6 +17,7 @@ import {
Input_Shadcn_,
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'
@@ -30,6 +32,10 @@ import { useOAuthServerAppsQuery } from '@/data/oauth-server-apps/oauth-server-a
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(),
@@ -80,8 +86,16 @@ export const OAuthServerSettingsForm = () => {
} = useAuthConfigQuery({ projectRef })
const { mutate: updateAuthConfig, isPending } = useAuthConfigUpdateMutation({
onSuccess: () => {
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}`)
@@ -182,134 +196,41 @@ export const OAuthServerSettingsForm = () => {
}
if (isAuthConfigLoading || isLoadingPermissions) {
return <GenericSkeletonLoader />
return (
<PageSection>
<PageSectionContent>
<Card>
<CardContent>
<GenericSkeletonLoader />
</CardContent>
</Card>
<OAuthEndpointsTable isLoading />
</PageSectionContent>
</PageSection>
)
}
return (
<>
<Form_Shadcn_ {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<Card>
<CardContent>
<FormField_Shadcn_
control={form.control}
name="OAUTH_SERVER_ENABLED"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Enable the Supabase OAuth Server"
description={
<>
Enable OAuth server functionality for your project to create and manage
OAuth applications.{' '}
<InlineLink href={`${DOCS_URL}/guides/auth/oauth-server`}>
Learn more
</InlineLink>
</>
}
>
<FormControl_Shadcn_>
<Switch
checked={field.value}
onCheckedChange={handleOAuthServerToggle}
disabled={!canUpdateConfig}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</CardContent>
{/* Site URL and Authorization Path - Only show when OAuth Server is enabled */}
{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_Shadcn_
value={authConfig?.SITE_URL}
disabled
placeholder="https://example.com"
/>
</FormItemLayout>
</CardContent>
<CardContent className="space-y-4">
<FormField_Shadcn_
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_Shadcn_>
<Input_Shadcn_ {...field} placeholder="/auth/authorize" />
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
{(() => {
const authorizationUrl = `${authConfig?.SITE_URL}${form.watch('OAUTH_SERVER_AUTHORIZATION_PATH') || '/oauth/consent'}`
return (
<Admonition
type="tip"
title="Make sure this path is implemented in your application."
description={
<>
Preview Authorization URL:{' '}
<a
href={authorizationUrl}
target="_blank"
rel="noreferrer"
className="text-foreground-light underline hover:text-foreground transition"
>
{authorizationUrl}
</a>
</>
}
/>
)
})()}
</CardContent>
<PageSection>
<PageSectionContent>
<Form_Shadcn_ {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<Card>
<CardContent>
<FormField_Shadcn_
control={form.control}
name="OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION"
name="OAUTH_SERVER_ENABLED"
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>
</>
}
label="Enable the Supabase OAuth Server"
description="Enable OAuth server functionality for your project to create and manage OAuth applications."
>
<FormControl_Shadcn_>
<Switch
checked={field.value}
onCheckedChange={handleDynamicAppsToggle}
onCheckedChange={handleOAuthServerToggle}
disabled={!canUpdateConfig}
/>
</FormControl_Shadcn_>
@@ -317,25 +238,137 @@ export const OAuthServerSettingsForm = () => {
)}
/>
</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_Shadcn_
value={authConfig?.SITE_URL}
disabled
placeholder="https://example.com"
/>
</FormItemLayout>
</CardContent>
<CardContent className="space-y-4">
<FormField_Shadcn_
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_Shadcn_>
<Input_Shadcn_ {...field} placeholder="/auth/authorize" />
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
{(() => {
const siteUrl = authConfig?.SITE_URL?.trim()
const authorizationPath =
form.watch('OAUTH_SERVER_AUTHORIZATION_PATH')?.trim() || '/oauth/consent'
const authorizationUrl = siteUrl ? `${siteUrl}${authorizationPath}` : ''
<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_Shadcn_>
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_Shadcn_
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_Shadcn_>
<Switch
checked={field.value}
onCheckedChange={handleDynamicAppsToggle}
disabled={!canUpdateConfig}
/>
</FormControl_Shadcn_>
</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_Shadcn_>
</PageSectionContent>
</PageSection>
{isSuccess && authConfig?.OAUTH_SERVER_ENABLED && form.watch('OAUTH_SERVER_ENABLED') && (
<OAuthEndpointsTable isLoading={isPending} />
)}
{/* Dynamic Apps Confirmation Modal */}
<ConfirmationModal
@@ -2,4 +2,6 @@ export const oauthServerAppKeys = {
// temporaryApiKey has to be added to reset the query when it changes
list: (projectRef: string | undefined, clientEndpoint: string | undefined) =>
['projects', projectRef, 'oauth-server-apps', clientEndpoint] as const,
openidConfiguration: (projectRef: string | undefined) =>
['projects', projectRef, 'oauth-server-openid-configuration'] as const,
}
@@ -0,0 +1,91 @@
import { useQuery } from '@tanstack/react-query'
import { useAuthConfigQuery } from '../auth/auth-config-query'
import { oauthServerAppKeys } from './keys'
import { useProjectApiUrl } from '@/data/config/project-endpoint-query'
import { handleError } from '@/data/fetchers'
import type { ResponseError, UseCustomQueryOptions } from '@/types'
export type OpenIDConfigurationVariables = {
projectRef: string | undefined
}
/**
* OpenID Connect Discovery response
* @see https://openid.net/specs/openid-connect-discovery-1_0.html
*/
export type OpenIDConfiguration = {
issuer: string
authorization_endpoint: string
token_endpoint: string
userinfo_endpoint?: string
jwks_uri: string
registration_endpoint?: string
scopes_supported?: string[]
response_types_supported?: string[]
grant_types_supported?: string[]
subject_types_supported?: string[]
id_token_signing_alg_values_supported?: string[]
token_endpoint_auth_methods_supported?: string[]
code_challenge_methods_supported?: string[]
}
export async function getOpenIDConfiguration({
clientEndpoint,
}: {
clientEndpoint: string | undefined
}): Promise<OpenIDConfiguration> {
if (!clientEndpoint) throw new Error('Client endpoint is required')
const response = await fetch(`${clientEndpoint}/auth/v1/.well-known/openid-configuration`)
if (!response.ok) {
handleError({ message: `Failed to fetch OpenID configuration: ${response.statusText}` })
}
return response.json()
}
export type OpenIDConfigurationData = Awaited<ReturnType<typeof getOpenIDConfiguration>>
export type OpenIDConfigurationError = ResponseError
export const useOpenIDConfigurationQuery = <TData = OpenIDConfigurationData>(
{ projectRef }: OpenIDConfigurationVariables,
{
enabled = true,
...options
}: UseCustomQueryOptions<OpenIDConfigurationData, OpenIDConfigurationError, TData> = {}
) => {
const { data: clientEndpoint, isPending: isEndpointLoading } = useProjectApiUrl({
projectRef,
})
const {
data: authConfig,
isSuccess: isSuccessConfig,
isLoading: isAuthConfigLoading,
} = useAuthConfigQuery({ projectRef })
const isOAuthServerEnabled = !!authConfig?.OAUTH_SERVER_ENABLED
const isQueryEnabled =
enabled &&
typeof projectRef !== 'undefined' &&
!!clientEndpoint &&
isSuccessConfig &&
isOAuthServerEnabled
const query = useQuery<OpenIDConfigurationData, OpenIDConfigurationError, TData>({
queryKey: oauthServerAppKeys.openidConfiguration(projectRef),
queryFn: () => getOpenIDConfiguration({ clientEndpoint }),
enabled: isQueryEnabled,
...options,
})
// Include loading states from dependencies
const isLoading = query.isLoading || isEndpointLoading || isAuthConfigLoading
return {
...query,
isLoading,
}
}
@@ -33,11 +33,7 @@ const ProvidersPage: NextPageWithLayout = () => {
</PageHeaderMeta>
</PageHeader>
<PageContainer size="default">
<PageSection>
<PageSectionContent>
<OAuthServerSettingsForm />
</PageSectionContent>
</PageSection>
<OAuthServerSettingsForm />
</PageContainer>
</>
)