mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-30 03:25:22 +02:00
feat: implement forward authentication settings and UI components
- Added a new `forward_auth_settings` table to manage authentication domains and their configurations. - Introduced UI components for handling forward authentication, including enabling/disabling SSO for domains and selecting SSO providers. - Updated existing tests to include validation for the new `forwardAuthProviderId` field in domain configurations. - Enhanced the dashboard to integrate forward authentication management, allowing users to configure SSO settings directly from the application interface. This update improves the flexibility and security of application authentication by allowing integration with various identity providers.
This commit is contained in:
@@ -10,6 +10,11 @@ import {
|
||||
writeTraefikConfigRemote,
|
||||
} from "./application";
|
||||
import type { FileConfig, HttpRouter } from "./file-types";
|
||||
import {
|
||||
createForwardAuthMiddleware,
|
||||
forwardAuthMiddlewareName,
|
||||
removeForwardAuthMiddleware,
|
||||
} from "./forward-auth";
|
||||
import { createPathMiddlewares, removePathMiddlewares } from "./middleware";
|
||||
|
||||
export const manageDomain = async (app: ApplicationNested, domain: Domain) => {
|
||||
@@ -48,6 +53,10 @@ export const manageDomain = async (app: ApplicationNested, domain: Domain) => {
|
||||
config.http.services[serviceName] = createServiceConfig(appName, domain);
|
||||
|
||||
await createPathMiddlewares(app, domain);
|
||||
// SSO forward-auth: writes the per-app forwardAuth + errors middlewares (the
|
||||
// /oauth2/* router lives on the central auth domain, not here). No-op unless
|
||||
// the domain links a provider and the org has an auth domain configured.
|
||||
await createForwardAuthMiddleware(app, domain);
|
||||
|
||||
if (app.serverId) {
|
||||
await writeTraefikConfigRemote(config, appName, app.serverId);
|
||||
@@ -84,6 +93,7 @@ export const removeDomain = async (
|
||||
}
|
||||
|
||||
await removePathMiddlewares(application, uniqueKey);
|
||||
await removeForwardAuthMiddleware(application, uniqueKey);
|
||||
|
||||
// verify if is the last router if so we delete the router
|
||||
if (
|
||||
@@ -184,6 +194,16 @@ export const createRouterConfig = async (
|
||||
routerConfig.middlewares?.push(middlewareName);
|
||||
}
|
||||
|
||||
// Enterprise SSO forward-auth gate. Placed before custom middlewares so
|
||||
// authentication runs first. No-op unless the domain links a provider.
|
||||
// The -errors middleware must come first so a 401 from the auth check is
|
||||
// rewritten to a 302 redirect to the login page.
|
||||
if (domain.forwardAuthProviderId) {
|
||||
const name = forwardAuthMiddlewareName(appName, uniqueConfigKey);
|
||||
routerConfig.middlewares?.push(`${name}-errors`);
|
||||
routerConfig.middlewares?.push(name);
|
||||
}
|
||||
|
||||
// custom middlewares from domain
|
||||
if (domain.middlewares && domain.middlewares.length > 0) {
|
||||
routerConfig.middlewares?.push(...domain.middlewares);
|
||||
|
||||
@@ -652,6 +652,13 @@ export interface ErrorsMiddleware {
|
||||
* The URL for the error page (hosted by service). You can use {status} in the query, that will be replaced by the received status code.
|
||||
*/
|
||||
query?: string;
|
||||
/**
|
||||
* Rewrites the returning status code, mapping the original status to a new one
|
||||
* (e.g. { "401": 302 } so the browser follows the redirect to the login page).
|
||||
*/
|
||||
statusRewrites?: {
|
||||
[k: string]: number;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* The ForwardAuth middleware delegate the authentication to an external service. If the service response code is 2XX, access is granted and the original request is performed. Otherwise, the response from the authentication server is returned.
|
||||
|
||||
204
packages/server/src/utils/traefik/forward-auth.ts
Normal file
204
packages/server/src/utils/traefik/forward-auth.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
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.forwardAuthProviderId) {
|
||||
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);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user