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==}