From 82f798f75ad5ea25d0740fad7518d03c024ea7b7 Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Thu, 9 Apr 2026 16:01:26 +0200 Subject: [PATCH] chore(studio): show oauth server endpoints in oauth server settings (#41783) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Screenshot 2026-01-09 at 12 00 31 ## 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. ✏️ Tip: You can customize this high-level summary in your review settings. --------- Co-authored-by: Ali Waseem --- .../Auth/OAuthApps/OAuthEndpointsTable.tsx | 90 ++++++ .../OAuthApps/OAuthServerSettingsForm.tsx | 299 ++++++++++-------- apps/studio/data/oauth-server-apps/keys.ts | 2 + .../oauth-openid-configuration-query.ts | 91 ++++++ .../pages/project/[ref]/auth/oauth-server.tsx | 6 +- 5 files changed, 350 insertions(+), 138 deletions(-) create mode 100644 apps/studio/components/interfaces/Auth/OAuthApps/OAuthEndpointsTable.tsx create mode 100644 apps/studio/data/oauth-server-apps/oauth-openid-configuration-query.ts diff --git a/apps/studio/components/interfaces/Auth/OAuthApps/OAuthEndpointsTable.tsx b/apps/studio/components/interfaces/Auth/OAuthApps/OAuthEndpointsTable.tsx new file mode 100644 index 00000000000..c148a09e5e2 --- /dev/null +++ b/apps/studio/components/interfaces/Auth/OAuthApps/OAuthEndpointsTable.tsx @@ -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 ( + + + + OAuth Endpoints + + Share these endpoints with third-party applications that need to integrate with your + OAuth 2.1 server. + + + + + + + {isLoading ? ( + + ) : ( + endpoints.map((endpoint) => ( + + + + )) + )} + + + + + ) +} diff --git a/apps/studio/components/interfaces/Auth/OAuthApps/OAuthServerSettingsForm.tsx b/apps/studio/components/interfaces/Auth/OAuthApps/OAuthServerSettingsForm.tsx index c87d0786292..0feed4f1f16 100644 --- a/apps/studio/components/interfaces/Auth/OAuthApps/OAuthServerSettingsForm.tsx +++ b/apps/studio/components/interfaces/Auth/OAuthApps/OAuthServerSettingsForm.tsx @@ -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 + return ( + + + + + + + + + + + ) } return ( <> - -
- - - ( - - Enable OAuth server functionality for your project to create and manage - OAuth applications.{' '} - - Learn more - - - } - > - - - - - )} - /> - - {/* Site URL and Authorization Path - Only show when OAuth Server is enabled */} - {form.watch('OAUTH_SERVER_ENABLED') && ( - <> - - - The base URL of your application, configured in{' '} - - Auth URL Configuration - {' '} - settings. - - } - > - - - - - ( - - - - - - )} - /> - {(() => { - const authorizationUrl = `${authConfig?.SITE_URL}${form.watch('OAUTH_SERVER_AUTHORIZATION_PATH') || '/oauth/consent'}` - return ( - - Preview Authorization URL:{' '} - - {authorizationUrl} - - - } - /> - ) - })()} - + + + + + ( - Enable dynamic OAuth app registration. Apps can be registered - programmatically via APIs.{' '} - - Learn more - - - } + label="Enable the Supabase OAuth Server" + description="Enable OAuth server functionality for your project to create and manage OAuth applications." > @@ -317,25 +238,137 @@ export const OAuthServerSettingsForm = () => { )} /> - - )} + {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 */} ['projects', projectRef, 'oauth-server-apps', clientEndpoint] as const, + openidConfiguration: (projectRef: string | undefined) => + ['projects', projectRef, 'oauth-server-openid-configuration'] as const, } diff --git a/apps/studio/data/oauth-server-apps/oauth-openid-configuration-query.ts b/apps/studio/data/oauth-server-apps/oauth-openid-configuration-query.ts new file mode 100644 index 00000000000..71c6a8295d8 --- /dev/null +++ b/apps/studio/data/oauth-server-apps/oauth-openid-configuration-query.ts @@ -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 { + 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> +export type OpenIDConfigurationError = ResponseError + +export const useOpenIDConfigurationQuery = ( + { projectRef }: OpenIDConfigurationVariables, + { + enabled = true, + ...options + }: UseCustomQueryOptions = {} +) => { + 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({ + queryKey: oauthServerAppKeys.openidConfiguration(projectRef), + queryFn: () => getOpenIDConfiguration({ clientEndpoint }), + enabled: isQueryEnabled, + ...options, + }) + + // Include loading states from dependencies + const isLoading = query.isLoading || isEndpointLoading || isAuthConfigLoading + + return { + ...query, + isLoading, + } +} diff --git a/apps/studio/pages/project/[ref]/auth/oauth-server.tsx b/apps/studio/pages/project/[ref]/auth/oauth-server.tsx index b304244d6fa..8aed4d6af19 100644 --- a/apps/studio/pages/project/[ref]/auth/oauth-server.tsx +++ b/apps/studio/pages/project/[ref]/auth/oauth-server.tsx @@ -33,11 +33,7 @@ const ProvidersPage: NextPageWithLayout = () => { - - - - - + )