mirror of
https://github.com/andwati/dokploy.git
synced 2026-10-21 04:24:30 +00:00
- 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.
234 lines
7.1 KiB
TypeScript
234 lines
7.1 KiB
TypeScript
import type { ApplicationNested, Domain } from "@dokploy/server";
|
|
import {
|
|
buildForwardAuthEnv,
|
|
createRouterConfig,
|
|
deriveBaseDomain,
|
|
deriveCookieSecret,
|
|
forwardAuthCallbackUrl,
|
|
forwardAuthMiddlewareName,
|
|
} from "@dokploy/server";
|
|
import { beforeAll, describe, expect, test } from "vitest";
|
|
|
|
const app = {
|
|
appName: "my-app",
|
|
redirects: [],
|
|
security: [],
|
|
} as unknown as ApplicationNested;
|
|
|
|
const baseDomain: Domain = {
|
|
applicationId: "app-1",
|
|
certificateType: "none",
|
|
createdAt: "",
|
|
domainId: "domain-1",
|
|
host: "app.example.com",
|
|
https: false,
|
|
path: null,
|
|
port: 3000,
|
|
customEntrypoint: null,
|
|
serviceName: "",
|
|
composeId: "",
|
|
customCertResolver: null,
|
|
domainType: "application",
|
|
uniqueConfigKey: 7,
|
|
previewDeploymentId: "",
|
|
internalPath: "/",
|
|
stripPath: false,
|
|
middlewares: null,
|
|
forwardAuthEnabled: false,
|
|
};
|
|
|
|
describe("forwardAuthMiddlewareName", () => {
|
|
test("is stable and unique per app + uniqueConfigKey", () => {
|
|
expect(forwardAuthMiddlewareName("my-app", 7)).toBe(
|
|
"forward-auth-my-app-7",
|
|
);
|
|
expect(forwardAuthMiddlewareName("my-app", 7)).toBe(
|
|
forwardAuthMiddlewareName("my-app", 7),
|
|
);
|
|
expect(forwardAuthMiddlewareName("my-app", 7)).not.toBe(
|
|
forwardAuthMiddlewareName("my-app", 8),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("createRouterConfig forward-auth wiring", () => {
|
|
test("does NOT add forward-auth middleware when no provider is linked", async () => {
|
|
const config = await createRouterConfig(app, baseDomain, "websecure");
|
|
expect(config.middlewares).not.toContain(
|
|
forwardAuthMiddlewareName("my-app", 7),
|
|
);
|
|
});
|
|
|
|
test("adds forward-auth middleware when a provider is linked", async () => {
|
|
const domain: Domain = {
|
|
...baseDomain,
|
|
forwardAuthEnabled: true,
|
|
};
|
|
const config = await createRouterConfig(app, domain, "websecure");
|
|
expect(config.middlewares).toContain(
|
|
forwardAuthMiddlewareName("my-app", 7),
|
|
);
|
|
});
|
|
|
|
test("forward-auth runs before custom domain middlewares", async () => {
|
|
const domain: Domain = {
|
|
...baseDomain,
|
|
forwardAuthEnabled: true,
|
|
middlewares: ["rate-limit@file"],
|
|
};
|
|
const config = await createRouterConfig(app, domain, "websecure");
|
|
const forwardAuthIdx = config.middlewares?.indexOf(
|
|
forwardAuthMiddlewareName("my-app", 7),
|
|
);
|
|
const customIdx = config.middlewares?.indexOf("rate-limit@file");
|
|
expect(forwardAuthIdx).toBeGreaterThanOrEqual(0);
|
|
expect(customIdx).toBeGreaterThan(forwardAuthIdx as number);
|
|
});
|
|
|
|
test("redirect-only web router does not get the forward-auth middleware", async () => {
|
|
const domain: Domain = {
|
|
...baseDomain,
|
|
https: true,
|
|
forwardAuthEnabled: true,
|
|
};
|
|
const config = await createRouterConfig(app, domain, "web");
|
|
expect(config.middlewares).toContain("redirect-to-https");
|
|
expect(config.middlewares).not.toContain(
|
|
forwardAuthMiddlewareName("my-app", 7),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("buildForwardAuthEnv", () => {
|
|
const baseOptions = {
|
|
oidc: {
|
|
clientId: "client-123",
|
|
clientSecret: "secret-xyz",
|
|
issuer: "https://idp.example.com",
|
|
},
|
|
cookieSecret: "cookie-secret-value",
|
|
authDomain: "auth.acme.com",
|
|
baseDomain: ".acme.com",
|
|
authDomainHttps: true,
|
|
};
|
|
|
|
test("emits the required oauth2-proxy OIDC env vars", () => {
|
|
const env = buildForwardAuthEnv(baseOptions);
|
|
expect(env).toContain("OAUTH2_PROXY_PROVIDER=oidc");
|
|
expect(env).toContain(
|
|
"OAUTH2_PROXY_OIDC_ISSUER_URL=https://idp.example.com",
|
|
);
|
|
expect(env).toContain("OAUTH2_PROXY_CLIENT_ID=client-123");
|
|
expect(env).toContain("OAUTH2_PROXY_CLIENT_SECRET=secret-xyz");
|
|
expect(env).toContain("OAUTH2_PROXY_COOKIE_SECRET=cookie-secret-value");
|
|
expect(env).toContain("OAUTH2_PROXY_REVERSE_PROXY=true");
|
|
expect(env).toContain("OAUTH2_PROXY_HTTP_ADDRESS=0.0.0.0:4180");
|
|
});
|
|
|
|
test("uses the central auth domain for the single fixed callback", () => {
|
|
const env = buildForwardAuthEnv(baseOptions);
|
|
expect(env).toContain(
|
|
"OAUTH2_PROXY_REDIRECT_URL=https://auth.acme.com/oauth2/callback",
|
|
);
|
|
});
|
|
|
|
test("shares cookie + whitelist on the base domain (no per-app redeploy)", () => {
|
|
const env = buildForwardAuthEnv(baseOptions);
|
|
expect(env).toContain("OAUTH2_PROXY_COOKIE_DOMAINS=.acme.com");
|
|
expect(env).toContain("OAUTH2_PROXY_WHITELIST_DOMAINS=.acme.com");
|
|
});
|
|
|
|
test("matches cookie Secure flag and callback scheme to https setting", () => {
|
|
const https = buildForwardAuthEnv(baseOptions);
|
|
expect(https).toContain("OAUTH2_PROXY_COOKIE_SECURE=true");
|
|
|
|
const http = buildForwardAuthEnv({
|
|
...baseOptions,
|
|
authDomainHttps: false,
|
|
});
|
|
expect(http).toContain("OAUTH2_PROXY_COOKIE_SECURE=false");
|
|
expect(http).toContain(
|
|
"OAUTH2_PROXY_REDIRECT_URL=http://auth.acme.com/oauth2/callback",
|
|
);
|
|
});
|
|
|
|
test("allows unverified emails so OIDC providers don't 500 the callback", () => {
|
|
const env = buildForwardAuthEnv(baseOptions);
|
|
expect(env).toContain(
|
|
"OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL=true",
|
|
);
|
|
});
|
|
|
|
test("defaults to any authenticated user and standard scopes", () => {
|
|
const env = buildForwardAuthEnv(baseOptions);
|
|
expect(env).toContain("OAUTH2_PROXY_EMAIL_DOMAINS=*");
|
|
expect(env).toContain("OAUTH2_PROXY_SCOPE=openid email profile");
|
|
});
|
|
|
|
test("honors custom scopes and email domains", () => {
|
|
const env = buildForwardAuthEnv({
|
|
...baseOptions,
|
|
oidc: { ...baseOptions.oidc, scopes: ["openid", "groups"] },
|
|
emailDomains: ["acme.com", "corp.com"],
|
|
});
|
|
expect(env).toContain("OAUTH2_PROXY_SCOPE=openid groups");
|
|
expect(env).toContain("OAUTH2_PROXY_EMAIL_DOMAINS=acme.com,corp.com");
|
|
});
|
|
|
|
test("sets skip-discovery flag only when requested", () => {
|
|
const withoutSkip = buildForwardAuthEnv(baseOptions);
|
|
expect(withoutSkip).not.toContain("OAUTH2_PROXY_SKIP_OIDC_DISCOVERY=true");
|
|
|
|
const withSkip = buildForwardAuthEnv({
|
|
...baseOptions,
|
|
oidc: { ...baseOptions.oidc, skipDiscovery: true },
|
|
});
|
|
expect(withSkip).toContain("OAUTH2_PROXY_SKIP_OIDC_DISCOVERY=true");
|
|
});
|
|
});
|
|
|
|
describe("deriveBaseDomain", () => {
|
|
test("strips the auth subdomain to the shared base", () => {
|
|
expect(deriveBaseDomain("auth.acme.com")).toBe(".acme.com");
|
|
expect(deriveBaseDomain("sso.apps.acme.com")).toBe(".apps.acme.com");
|
|
});
|
|
|
|
test("keeps a two-label apex as the base", () => {
|
|
expect(deriveBaseDomain("acme.com")).toBe(".acme.com");
|
|
});
|
|
});
|
|
|
|
describe("forwardAuthCallbackUrl", () => {
|
|
test("builds the single IdP callback per scheme", () => {
|
|
expect(forwardAuthCallbackUrl("auth.acme.com", true)).toBe(
|
|
"https://auth.acme.com/oauth2/callback",
|
|
);
|
|
expect(forwardAuthCallbackUrl("auth.acme.com", false)).toBe(
|
|
"http://auth.acme.com/oauth2/callback",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("deriveCookieSecret", () => {
|
|
beforeAll(() => {
|
|
process.env.BETTER_AUTH_SECRET = "test-root-secret";
|
|
});
|
|
|
|
test("is deterministic for the same salt (survives service updates)", () => {
|
|
expect(deriveCookieSecret(".acme.com")).toBe(
|
|
deriveCookieSecret(".acme.com"),
|
|
);
|
|
});
|
|
|
|
test("differs per salt", () => {
|
|
expect(deriveCookieSecret(".acme.com")).not.toBe(
|
|
deriveCookieSecret(".other.com"),
|
|
);
|
|
});
|
|
|
|
test("produces a 32-byte base64 secret (oauth2-proxy requirement)", () => {
|
|
const secret = deriveCookieSecret(".acme.com");
|
|
expect(Buffer.from(secret, "base64")).toHaveLength(32);
|
|
});
|
|
});
|