import { createHmac } from "node:crypto"; import type { CreateServiceOptions } from "dockerode"; import { betterAuthSecret } from "../lib/auth-secret"; import { getRemoteDocker } from "../utils/servers/remote-docker"; export const FORWARD_AUTH_SERVICE_NAME = "dokploy-forward-auth"; const FORWARD_AUTH_IMAGE = "quay.io/oauth2-proxy/oauth2-proxy:v7.6.0"; export const FORWARD_AUTH_PORT = 4180; export interface ForwardAuthOidcConfig { clientId: string; clientSecret: string; issuer: string; scopes?: string[]; skipDiscovery?: boolean; } export interface SetupForwardAuthOptions { serverId?: string; oidc: ForwardAuthOidcConfig; cookieSecret: string; authDomain: string; baseDomain: string; authDomainHttps?: boolean; emailDomains?: string[]; } export const deriveBaseDomain = (authDomain: string): string => { const labels = authDomain.trim().toLowerCase().split(".").filter(Boolean); const base = labels.length > 2 ? labels.slice(1) : labels; return `.${base.join(".")}`; }; export const forwardAuthCallbackUrl = ( authDomain: string, https: boolean, ): string => `${https ? "https" : "http"}://${authDomain}/oauth2/callback`; export const deriveCookieSecret = (salt: string): string => { // oauth2-proxy requires cookie_secret to be exactly 16, 24, or 32 bytes. // Take the first 32 hex chars (= 16 bytes) to satisfy that constraint. return createHmac("sha256", betterAuthSecret) .update(`forward-auth:${salt}`) .digest("hex") .slice(0, 32); }; export const buildForwardAuthEnv = ( options: SetupForwardAuthOptions, ): string[] => { const { oidc, cookieSecret, authDomain, baseDomain, authDomainHttps } = options; const scheme = authDomainHttps ? "https" : "http"; const emailDomains = options.emailDomains && options.emailDomains.length > 0 ? options.emailDomains : ["*"]; const env: string[] = [ "OAUTH2_PROXY_PROVIDER=oidc", `OAUTH2_PROXY_OIDC_ISSUER_URL=${oidc.issuer}`, `OAUTH2_PROXY_CLIENT_ID=${oidc.clientId}`, `OAUTH2_PROXY_CLIENT_SECRET=${oidc.clientSecret}`, `OAUTH2_PROXY_COOKIE_SECRET=${cookieSecret}`, `OAUTH2_PROXY_HTTP_ADDRESS=0.0.0.0:${FORWARD_AUTH_PORT}`, "OAUTH2_PROXY_REVERSE_PROXY=true", "OAUTH2_PROXY_SKIP_PROVIDER_BUTTON=true", "OAUTH2_PROXY_SET_XAUTHREQUEST=true", "OAUTH2_PROXY_UPSTREAMS=static://202", `OAUTH2_PROXY_REDIRECT_URL=${scheme}://${authDomain}/oauth2/callback`, `OAUTH2_PROXY_COOKIE_DOMAINS=${baseDomain}`, `OAUTH2_PROXY_WHITELIST_DOMAINS=${baseDomain}`, `OAUTH2_PROXY_COOKIE_SECURE=${authDomainHttps ? "true" : "false"}`, "OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL=true", `OAUTH2_PROXY_EMAIL_DOMAINS=${emailDomains.join(",")}`, ]; const scopes = oidc.scopes?.length ? oidc.scopes : ["openid", "email", "profile"]; env.push(`OAUTH2_PROXY_SCOPE=${scopes.join(" ")}`); if (oidc.skipDiscovery) { env.push("OAUTH2_PROXY_SKIP_OIDC_DISCOVERY=true"); } return env; }; export const setupForwardAuth = async (options: SetupForwardAuthOptions) => { const { serverId } = options; const docker = await getRemoteDocker(serverId); const settings: CreateServiceOptions = { Name: FORWARD_AUTH_SERVICE_NAME, TaskTemplate: { ContainerSpec: { Image: FORWARD_AUTH_IMAGE, Env: buildForwardAuthEnv(options), }, Networks: [{ Target: "dokploy-network" }], Placement: { Constraints: ["node.role==manager"], }, }, Mode: { Replicated: { Replicas: 1, }, }, }; try { const service = docker.getService(FORWARD_AUTH_SERVICE_NAME); const inspect = await service.inspect(); await service.update({ version: Number.parseInt(inspect.Version.Index), ...settings, TaskTemplate: { ...settings.TaskTemplate, ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1, }, }); console.log("Forward Auth Updated ✅"); } catch (_) { try { await docker.createService(settings); console.log("Forward Auth Started ✅"); } catch (error: any) { if (error?.statusCode !== 409) { throw error; } console.log("Forward Auth service already exists, continuing..."); } } }; export const removeForwardAuth = async (serverId?: string) => { const docker = await getRemoteDocker(serverId); try { const service = docker.getService(FORWARD_AUTH_SERVICE_NAME); await service.remove(); console.log("Forward Auth Removed ✅"); } catch {} }; export const isForwardAuthRunning = async ( serverId?: string, ): Promise => { const docker = await getRemoteDocker(serverId); try { await docker.getService(FORWARD_AUTH_SERVICE_NAME).inspect(); return true; } catch { return false; } };