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 (
<>
-
-
+
+
+
+ {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 = () => {
-
-
-
-
-
+
>
)