Files
dokploy/packages/server/src/utils/traefik/forward-auth.ts
Mauricio Siu 1df6774ee8 refactor: update forward authentication handling in domain schema and tests
- Replaced `forwardAuthProviderId` with `forwardAuthEnabled` in the domain schema to simplify the configuration of forward authentication.
- Updated related tests to reflect this change, ensuring consistency across the application.
- Introduced a new SQL migration to create the `forward_auth_settings` table for managing authentication domains and their configurations.

This refactor enhances the clarity and maintainability of the forward authentication logic within the application.
2026-06-06 03:53:45 -06:00

205 lines
5.0 KiB
TypeScript

import { db } from "@dokploy/server/db";
import { forwardAuthSettings } from "@dokploy/server/db/schema";
import type { Domain } from "@dokploy/server/services/domain";
import {
FORWARD_AUTH_PORT,
FORWARD_AUTH_SERVICE_NAME,
} from "@dokploy/server/setup/forward-auth-setup";
import { eq, isNull } from "drizzle-orm";
import type { ApplicationNested } from "../builders";
import {
removeTraefikConfig,
removeTraefikConfigRemote,
writeTraefikConfig,
writeTraefikConfigRemote,
} from "./application";
import type { FileConfig } from "./file-types";
import {
loadMiddlewares,
loadRemoteMiddlewares,
writeMiddleware,
} from "./middleware";
export interface AuthDomainConfig {
authDomain: string;
https: boolean;
certificateType: "none" | "letsencrypt" | "custom";
customCertResolver?: string | null;
}
const TRAEFIK_SERVICE = "forward-auth-proxy";
export const forwardAuthMiddlewareName = (
appName: string,
uniqueConfigKey: number,
): string => `forward-auth-${appName}-${uniqueConfigKey}`;
const proxyUrl = () =>
`http://${FORWARD_AUTH_SERVICE_NAME}:${FORWARD_AUTH_PORT}`;
const loadOrEmptyMiddlewares = async (
serverId: string | null,
): Promise<FileConfig> => {
try {
return serverId
? await loadRemoteMiddlewares(serverId)
: loadMiddlewares<FileConfig>();
} catch {
return { http: { middlewares: {} } };
}
};
const persistMiddlewares = async (
config: FileConfig,
serverId: string | null,
) => {
if (serverId) {
await writeTraefikConfigRemote(config, "middlewares", serverId);
} else {
writeMiddleware(config);
}
};
const loadAuthGateDomain = async (serverId: string | null) => {
return db.query.forwardAuthSettings.findFirst({
where: serverId
? eq(forwardAuthSettings.serverId, serverId)
: isNull(forwardAuthSettings.serverId),
columns: { authDomain: true, https: true },
});
};
export const createForwardAuthMiddleware = async (
app: ApplicationNested,
domain: Domain,
) => {
if (!domain.forwardAuthEnabled) {
return;
}
const authGate = await loadAuthGateDomain(app.serverId ?? null);
if (!authGate) {
return;
}
const authDomain = authGate.authDomain;
const authDomainHttps = authGate.https;
const { appName, serverId } = app;
const config = await loadOrEmptyMiddlewares(serverId);
config.http = config.http || {};
config.http.middlewares = config.http.middlewares || {};
const name = forwardAuthMiddlewareName(appName, domain.uniqueConfigKey);
const scheme = authDomainHttps ? "https" : "http";
config.http.middlewares[name] = {
forwardAuth: {
address: `${scheme}://${authDomain}/oauth2/auth`,
trustForwardHeader: true,
authResponseHeaders: [
"X-Auth-Request-User",
"X-Auth-Request-Email",
"X-Auth-Request-Preferred-Username",
"Authorization",
],
},
};
config.http.middlewares[`${name}-errors`] = {
errors: {
status: ["401-403"],
service: TRAEFIK_SERVICE,
query: "/oauth2/sign_in?rd={url}",
statusRewrites: { "401": 302 },
},
};
await persistMiddlewares(config, serverId);
};
export const removeForwardAuthMiddleware = async (
app: ApplicationNested,
uniqueConfigKey: number,
) => {
const { appName, serverId } = app;
let config: FileConfig;
try {
config = serverId
? await loadRemoteMiddlewares(serverId)
: loadMiddlewares<FileConfig>();
} catch {
return;
}
const name = forwardAuthMiddlewareName(appName, uniqueConfigKey);
let changed = false;
for (const key of [name, `${name}-errors`]) {
if (config.http?.middlewares?.[key]) {
delete config.http.middlewares[key];
changed = true;
}
}
if (changed) {
await persistMiddlewares(config, serverId);
}
};
export const buildAuthDomainRouter = (cfg: AuthDomainConfig): FileConfig => {
const entry = cfg.https ? "websecure" : "web";
const oauthRouter: NonNullable<
NonNullable<FileConfig["http"]>["routers"]
>[string] = {
rule: `Host(\`${cfg.authDomain}\`) && PathPrefix(\`/oauth2/\`)`,
service: TRAEFIK_SERVICE,
entryPoints: [entry],
priority: 1000,
};
if (cfg.https) {
if (cfg.certificateType === "letsencrypt") {
oauthRouter.tls = { certResolver: "letsencrypt" };
} else if (cfg.certificateType === "custom" && cfg.customCertResolver) {
oauthRouter.tls = { certResolver: cfg.customCertResolver };
} else {
oauthRouter.tls = {};
}
}
return {
http: {
routers: { "forward-auth-oauth": oauthRouter },
services: {
[TRAEFIK_SERVICE]: {
loadBalancer: {
servers: [{ url: proxyUrl() }],
passHostHeader: true,
},
},
},
},
};
};
export const authDomainConfigName = "forward-auth-domain";
export const manageForwardAuthDomain = async (
serverId: string | null,
cfg: AuthDomainConfig,
) => {
const config = buildAuthDomainRouter(cfg);
if (serverId) {
await writeTraefikConfigRemote(config, authDomainConfigName, serverId);
} else {
writeTraefikConfig(config, authDomainConfigName);
}
};
export const removeForwardAuthDomain = async (serverId: string | null) => {
if (serverId) {
await removeTraefikConfigRemote(authDomainConfigName, serverId);
} else {
await removeTraefikConfig(authDomainConfigName);
}
};