From aa558b3a8cdb4c167dd5f4146d4fbcd1a13e4398 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 1 Feb 2026 19:50:33 -0600 Subject: [PATCH] feat(sso): update SAML registration dialog and settings for improved metadata handling - Added support for IdP metadata XML in the SAML registration dialog, allowing users to paste full metadata for configuration. - Updated the callback URL and audience handling to dynamically incorporate the base URL. - Refactored the SSO settings to enable SAML provider registration and improved the display of callback URLs based on provider details. - Enhanced trusted origins configuration in the authentication logic to include additional domains for development and production environments. --- .../proprietary/sso/register-saml-dialog.tsx | 78 +++++++++++-------- .../proprietary/sso/sso-settings.tsx | 13 ++-- packages/server/src/lib/auth.ts | 57 +++++++------- pnpm-lock.yaml | 1 + 4 files changed, 84 insertions(+), 65 deletions(-) diff --git a/apps/dokploy/components/proprietary/sso/register-saml-dialog.tsx b/apps/dokploy/components/proprietary/sso/register-saml-dialog.tsx index 839ff1dbc..4835eb6b8 100644 --- a/apps/dokploy/components/proprietary/sso/register-saml-dialog.tsx +++ b/apps/dokploy/components/proprietary/sso/register-saml-dialog.tsx @@ -2,7 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { Plus, Trash2 } from "lucide-react"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { type FieldArrayPath, useFieldArray, useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; @@ -52,12 +52,7 @@ const samlProviderSchema = z.object({ .url("Invalid URL") .trim(), cert: z.string().min(1, "IdP signing certificate is required"), - callbackUrl: z - .string() - .min(1, "Callback URL is required") - .url("Invalid URL") - .trim(), - audience: z.string().min(1, "Audience (Entity ID) is required").trim(), + idpMetadataXml: z.string().optional(), }); type SamlProviderForm = z.infer; @@ -72,8 +67,7 @@ const formDefaultValues: SamlProviderForm = { domains: [""], entryPoint: "", cert: "", - callbackUrl: "", - audience: "", + idpMetadataXml: "", }; export function RegisterSamlDialog({ children }: RegisterSamlDialogProps) { @@ -81,6 +75,14 @@ export function RegisterSamlDialog({ children }: RegisterSamlDialogProps) { const [open, setOpen] = useState(false); const { mutateAsync, isLoading } = api.sso.register.useMutation(); + const [baseURL, setBaseURL] = useState(""); + + useEffect(() => { + if (typeof window !== "undefined") { + setBaseURL(window.location.origin); + } + }, []); + const form = useForm({ resolver: zodResolver(samlProviderSchema), defaultValues: formDefaultValues, @@ -95,6 +97,17 @@ export function RegisterSamlDialog({ children }: RegisterSamlDialogProps) { const onSubmit = async (data: SamlProviderForm) => { try { + // maybe add the /saml/metadata endpoint to the baseURL + const baseURLWithMetadata = `${baseURL}/saml/metadata`; + const generateSpMetadata = (providerId: string) => { + return ` + + + + +`; + }; + await mutateAsync({ providerId: data.providerId, issuer: data.issuer, @@ -102,13 +115,20 @@ export function RegisterSamlDialog({ children }: RegisterSamlDialogProps) { samlConfig: { entryPoint: data.entryPoint, cert: data.cert, - callbackUrl: data.callbackUrl, - audience: data.audience, - wantAssertionsSigned: true, - signatureAlgorithm: "sha256", - digestAlgorithm: "sha256", + callbackUrl: `${baseURL}/api/auth/sso/saml2/callback/${data.providerId}`, + audience: baseURL, + idpMetadata: data.idpMetadataXml?.trim() + ? { metadata: data.idpMetadataXml.trim() } + : undefined, spMetadata: { - entityID: data.audience, + metadata: generateSpMetadata(data.providerId), + }, + mapping: { + id: "nameID", + email: "email", + name: "displayName", + firstName: "givenName", + lastName: "surname", }, }, }); @@ -264,39 +284,29 @@ export function RegisterSamlDialog({ children }: RegisterSamlDialogProps) { )} /> + ( - Callback URL (ACS) + IdP metadata XML (optional) - - Use the callback URL shown in your IdP app config for this - provider. + Some IdPs require full metadata; paste the XML here to + override issuer/entry point/cert. )} /> - ( - - Audience (Entity ID) - - - - - - )} - /> - {/* + - */} + )} @@ -234,12 +234,12 @@ export const SSOSettings = () => { Add OIDC provider - {/* + - */} + )} @@ -340,7 +340,10 @@ export const SSOSettings = () => { Callback URL (configure in your IdP)

- {baseURL || "{baseURL}"}/api/auth/sso/callback/ + {baseURL || "{baseURL}"} + {detailsProvider.samlConfig + ? "/api/auth/sso/saml2/callback/" + : "/api/auth/sso/callback/"} {detailsProvider.providerId}

{!baseURL && ( diff --git a/packages/server/src/lib/auth.ts b/packages/server/src/lib/auth.ts index b8b0409af..ac6b44e53 100644 --- a/packages/server/src/lib/auth.ts +++ b/packages/server/src/lib/auth.ts @@ -25,7 +25,7 @@ export const { handler, api } = betterAuth({ schema: schema, }), disabledPaths: [ - "/sso/register", + // "/sso/register", "/organization/create", "/organization/update", "/organization/delete", @@ -44,30 +44,35 @@ export const { handler, api } = betterAuth({ logger: { disabled: process.env.NODE_ENV === "production", }, - ...(!IS_CLOUD && { - async trustedOrigins() { - const settings = await getWebServerSettings(); - if (!settings) { - return []; - } + // ...(!IS_CLOUD && { + async trustedOrigins() { + const settings = await getWebServerSettings(); + if (!settings) { + return []; + } - const providers = await getSSOProviders(); - const domains = providers.map((provider) => provider.issuer); - return [ - ...(settings?.serverIp ? [`http://${settings?.serverIp}:3000`] : []), - ...(settings?.host ? [`https://${settings?.host}`] : []), - ...domains.map((domain) => domain), - ...(process.env.NODE_ENV === "development" - ? [ - "http://localhost:3000", - "https://absolutely-handy-falcon.ngrok-free.app", - "https://dev-pee8hhc3qbjlqedb.us.auth0.com", - "https://trial-2804699.okta.com", - ] - : []), - ]; - }, - }), + const providers = await getSSOProviders(); + const issuerOrigins = providers.map((provider) => provider.issuer); + + return [ + ...(settings?.serverIp ? [`http://${settings?.serverIp}:3000`] : []), + ...(settings?.host ? [`https://${settings?.host}`] : []), + ...issuerOrigins, + ...(process.env.NODE_ENV === "development" + ? [ + "http://localhost:3000", + "https://absolutely-handy-falcon.ngrok-free.app", + "https://dev-pee8hhc3qbjlqedb.us.auth0.com", + "https://trial-2804699.okta.com", + "https://login.microsoftonline.com", + "https://graph.microsoft.com", + ] + : []), + ]; + }, + // Untrusted OIDC discovery URL: The main discovery endpoint "https://login.microsoftonline.com/9f26c287-38e9-4731-9d1d-506365a6cc8e/.well-known/openid-configuration" is not trusted by your trusted origins configuration. + + // }), emailVerification: { sendOnSignUp: true, autoSignInAfterVerification: true, @@ -120,7 +125,7 @@ export const { handler, api } = betterAuth({ }); } } else { - const isSSORequest = context?.path.includes("/sso/callback"); + const isSSORequest = context?.path.includes("/sso"); if (isSSORequest) { return; } @@ -136,7 +141,7 @@ export const { handler, api } = betterAuth({ } }, after: async (user, context) => { - const isSSORequest = context?.path.includes("/sso/callback"); + const isSSORequest = context?.path.includes("/sso"); const isAdminPresent = await db.query.member.findFirst({ where: eq(schema.member.role, "owner"), }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b23e5a6fb..ad5f1f9cc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7637,6 +7637,7 @@ packages: tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me temporal-polyfill@0.2.5: resolution: {integrity: sha512-ye47xp8Cb0nDguAhrrDS1JT1SzwEV9e26sSsrWzVu+yPZ7LzceEcH0i2gci9jWfOfSCCgM3Qv5nOYShVUUFUXA==}