import { IS_CLOUD } from "@dokploy/server/constants"; import { db } from "@dokploy/server/db"; import { forwardAuthSettings, server, ssoProvider, } from "@dokploy/server/db/schema"; import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission"; import { deriveBaseDomain, deriveCookieSecret, type ForwardAuthOidcConfig, forwardAuthCallbackUrl, isForwardAuthRunning, removeForwardAuth, setupForwardAuth, } from "@dokploy/server/setup/forward-auth-setup"; import { manageDomain } from "@dokploy/server/utils/traefik/domain"; import { manageForwardAuthDomain, removeForwardAuthDomain, removeForwardAuthMiddleware, } from "@dokploy/server/utils/traefik/forward-auth"; import { TRPCError } from "@trpc/server"; import { and, asc, desc, eq, isNotNull, isNull } from "drizzle-orm"; import { findApplicationById } from "../application"; import { findDomainById, updateDomainById } from "../domain"; const resolveOidcConfig = (provider: { issuer: string; oidcConfig: string | null; }): ForwardAuthOidcConfig => { if (!provider.oidcConfig) { throw new TRPCError({ code: "BAD_REQUEST", message: "Forward-auth requires an OIDC provider — SAML is not supported.", }); } let parsed: any; try { parsed = JSON.parse(provider.oidcConfig); } catch { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to parse the SSO provider OIDC configuration", }); } if (!parsed?.clientId || !parsed?.clientSecret) { throw new TRPCError({ code: "BAD_REQUEST", message: "SSO provider OIDC config is missing clientId/clientSecret", }); } return { clientId: parsed.clientId, clientSecret: parsed.clientSecret, issuer: provider.issuer, scopes: parsed.scopes, skipDiscovery: parsed.skipDiscovery, }; }; const findProviderForOrg = async ( providerId: string, organizationId: string, ) => { const provider = await db.query.ssoProvider.findFirst({ where: and( eq(ssoProvider.providerId, providerId), eq(ssoProvider.organizationId, organizationId), ), columns: { providerId: true, issuer: true, oidcConfig: true }, }); if (!provider) { throw new TRPCError({ code: "NOT_FOUND", message: "SSO provider not found", }); } return provider; }; export const listSsoProvidersForOrg = async (organizationId: string) => { return db.query.ssoProvider.findMany({ where: and( eq(ssoProvider.organizationId, organizationId), isNotNull(ssoProvider.oidcConfig), ), columns: { providerId: true, issuer: true, domain: true }, orderBy: [asc(ssoProvider.createdAt)], }); }; export const getDomainSsoStatus = async ( ctx: { session: { activeOrganizationId: string } }, domainId: string, ) => { const domain = await findDomainById(domainId); if (domain.applicationId) { await checkServicePermissionAndAccess(ctx as any, domain.applicationId, { domain: ["read"], }); } return { enabled: !!domain.forwardAuthEnabled }; }; const settingsWhere = (serverId: string | null) => serverId ? eq(forwardAuthSettings.serverId, serverId) : isNull(forwardAuthSettings.serverId); export const getForwardAuthSettings = async (serverId: string | null) => { return db.query.forwardAuthSettings.findFirst({ where: settingsWhere(serverId), }); }; export const setForwardAuthSettings = async (input: { organizationId: string; serverId: string | null; authDomain: string; https: boolean; certificateType: "none" | "letsencrypt" | "custom"; customCertResolver?: string | null; }) => { const baseDomain = deriveBaseDomain(input.authDomain); const existing = await getForwardAuthSettings(input.serverId); const values = { authDomain: input.authDomain, baseDomain, https: input.https, certificateType: input.certificateType, customCertResolver: input.customCertResolver ?? null, }; if (existing) { await db .update(forwardAuthSettings) .set(values) .where(settingsWhere(input.serverId)); } else { await db.insert(forwardAuthSettings).values({ ...values, serverId: input.serverId, }); } await manageForwardAuthDomain(input.serverId, { authDomain: input.authDomain, https: input.https, certificateType: input.certificateType, customCertResolver: input.customCertResolver, }); if (existing?.providerId) { const proxyRunning = await isForwardAuthRunning( input.serverId ?? undefined, ); if (proxyRunning) { await deployForwardAuthOnServer({ serverId: input.serverId ?? undefined, providerId: existing.providerId, organizationId: input.organizationId, }); } } return { callbackUrl: forwardAuthCallbackUrl(input.authDomain, input.https) }; }; export const removeForwardAuthSettings = async (serverId: string | null) => { const existing = await getForwardAuthSettings(serverId); if (!existing) return { ok: true } as const; await removeForwardAuthDomain(serverId); await db.delete(forwardAuthSettings).where(settingsWhere(serverId)); return { ok: true } as const; }; export const deployForwardAuthOnServer = async (input: { serverId?: string; providerId: string; organizationId: string; }) => { const settings = await getForwardAuthSettings(input.serverId ?? null); if (!settings) { throw new TRPCError({ code: "PRECONDITION_FAILED", message: "Set the authentication domain for this server before deploying the proxy.", }); } const provider = await findProviderForOrg( input.providerId, input.organizationId, ); const oidc = resolveOidcConfig(provider); await setupForwardAuth({ serverId: input.serverId, oidc, cookieSecret: deriveCookieSecret( `${input.serverId ?? "host"}:${settings.baseDomain}`, ), authDomain: settings.authDomain, baseDomain: settings.baseDomain, authDomainHttps: settings.https, }); if (settings.providerId !== input.providerId) { await db .update(forwardAuthSettings) .set({ providerId: input.providerId }) .where(settingsWhere(input.serverId ?? null)); } return { ok: true } as const; }; const FORWARD_AUTH_CHECK_TIMEOUT_MS = 4000; const proxyStatus = async ( serverId: string | null, ): Promise<"running" | "stopped" | "unknown"> => { try { const running = await Promise.race([ isForwardAuthRunning(serverId ?? undefined), new Promise((_, reject) => setTimeout( () => reject(new Error("timeout")), FORWARD_AUTH_CHECK_TIMEOUT_MS, ), ), ]); return running ? "running" : "stopped"; } catch { return "unknown"; } }; export const getForwardAuthServerStatus = async (organizationId: string) => { const servers = await db.query.server.findMany({ where: and( eq(server.organizationId, organizationId), isNotNull(server.sshKeyId), eq(server.serverType, "deploy"), ), columns: { serverId: true, name: true, ipAddress: true }, orderBy: [desc(server.createdAt)], }); const targets: { serverId: string | null; name: string; ipAddress: string | null; }[] = [ ...(IS_CLOUD ? [] : [ { serverId: null, name: "Dokploy Server (local)", ipAddress: null, }, ]), ...servers.map((s) => ({ serverId: s.serverId, name: s.name, ipAddress: s.ipAddress, })), ]; return Promise.all( targets.map(async (t) => { const settings = await getForwardAuthSettings(t.serverId); return { ...t, status: await proxyStatus(t.serverId), authDomain: settings?.authDomain ?? null, https: settings?.https ?? true, certificateType: settings?.certificateType ?? "none", customCertResolver: settings?.customCertResolver ?? null, callbackUrl: settings ? forwardAuthCallbackUrl(settings.authDomain, settings.https) : null, }; }), ); }; export const removeForwardAuthProxy = async (serverId: string | null) => { await removeForwardAuth(serverId ?? undefined); await db .update(forwardAuthSettings) .set({ providerId: null }) .where(settingsWhere(serverId)); return { ok: true } as const; }; const resolveApplicationDomain = async (domainId: string) => { const domain = await findDomainById(domainId); if (!domain.applicationId) { throw new TRPCError({ code: "BAD_REQUEST", message: "SSO forward-auth is currently only supported on application domains", }); } const application = await findApplicationById(domain.applicationId); return { domain, application }; }; export const assertApplicationDomainAccess = async ( ctx: { session: { activeOrganizationId: string } }, domainId: string, action: "create" | "delete", ) => { const domain = await findDomainById(domainId); if (!domain.applicationId) { throw new TRPCError({ code: "BAD_REQUEST", message: "SSO forward-auth is currently only supported on application domains", }); } await checkServicePermissionAndAccess(ctx as any, domain.applicationId, { domain: [action], }); return domain; }; export const enableForwardAuthOnDomain = async (input: { domainId: string; }) => { const { application } = await resolveApplicationDomain(input.domainId); const serverId = application.serverId ?? undefined; const settings = await getForwardAuthSettings(serverId ?? null); if (!settings?.providerId) { throw new TRPCError({ code: "PRECONDITION_FAILED", message: "Deploy the authentication proxy for this server in SSO settings first.", }); } const proxyRunning = await isForwardAuthRunning(serverId); if (!proxyRunning) { throw new TRPCError({ code: "PRECONDITION_FAILED", message: "The authentication proxy is not deployed on this server. Deploy it in SSO settings first.", }); } await updateDomainById(input.domainId, { forwardAuthEnabled: true }); const domain = await findDomainById(input.domainId); await manageDomain(application, domain); return { ok: true } as const; }; export const disableForwardAuthOnDomain = async (input: { domainId: string; }) => { const { application, domain } = await resolveApplicationDomain( input.domainId, ); const uniqueConfigKey = domain.uniqueConfigKey; await updateDomainById(input.domainId, { forwardAuthEnabled: false }); const updated = await findDomainById(input.domainId); await manageDomain(application, updated); await removeForwardAuthMiddleware(application, uniqueConfigKey); return { ok: true } as const; };