Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
9f10f0f
fix(migrate-auth-secret): exit cleanly when there are no 2FA records
ngenohkevin May 12, 2026
a714e0f
Merge pull request #4394 from ngenohkevin/fix/migrate-auth-secret-exi…
Siumauricio May 12, 2026
754774e
feat(compose): add import from base64 in create service dropdown
Siumauricio May 12, 2026
63e33a2
[autofix.ci] apply automated fixes
autofix-ci[bot] May 12, 2026
7a568aa
Merge pull request #4395 from Dokploy/feat/import-compose-from-base64
Siumauricio May 12, 2026
f8fcf68
Enhance version synchronization workflow to include SDK repository
Siumauricio May 12, 2026
558d809
feat(deployment): add readLogs procedure to fetch deployment logs
Siumauricio May 13, 2026
aff200f
feat(deployment): add server access validation for deployment actions
Siumauricio May 13, 2026
67278d8
feat(organization): prevent inviting users with owner role
Siumauricio May 13, 2026
1fdbe87
feat(user): implement session cleanup on user update
Siumauricio May 13, 2026
a50f958
feat(settings): add copy button to server IP in web server settings (…
Siumauricio May 13, 2026
8d88a34
fix: copy Dokploy server IP when clicking server badge (#4390)
vadamk May 13, 2026
ef0cf9b
fix: responsive layout (#4391)
nhridoy May 13, 2026
6e342ee
fix: automatically converting username to lowercase both in creation …
Baker May 13, 2026
af8072d
fix: allow square brackets in zip path validation for Next.js dynamic…
Siumauricio May 22, 2026
b06138b
fix: prevent webhook deploy crash when commit data lacks modified fil…
Siumauricio May 22, 2026
f6e6e5c
fix: add type="button" to TooltipTrigger in form components to preven…
mixelburg May 22, 2026
34d38cf
fix: enable comment toggle shortcut in env variable editor (#4402) (#…
Siumauricio May 22, 2026
103e2f7
fix: add tls=true label for domains when certificateType is none (#40…
Siumauricio May 22, 2026
2f43f60
chore: update version to v0.29.5 in package.json
Siumauricio May 22, 2026
6675aa6
chore(deps): upgrade next to 16.2.6 (#4477)
jasael May 24, 2026
8018027
feat: add self-hosted enterprise restrictions (remote-servers-only, e…
Siumauricio May 30, 2026
4ba0f71
fix: grant create and delete SSH key permissions when canAccessToSSHK…
Siumauricio May 30, 2026
d7d6422
fix: use create permission for basic auth delete instead of delete (#…
Siumauricio May 30, 2026
ad680ae
fix: wrap long server names and keep actions menu visible (#4434)
pparage May 30, 2026
9bd4451
chore: update version to v0.29.6 in package.json
Siumauricio May 30, 2026
85211af
fix: preserve HOME in compose deploy so --with-registry-auth can read…
youcefzemmar May 30, 2026
d56a17c
Merge branch 'main' into canary
Siumauricio May 30, 2026
6ff2ca0
fix: scope dokploy-server schedules to organization instead of user (…
Siumauricio May 31, 2026
41c09cd
feat: implement forward authentication settings and UI components
Siumauricio Jun 2, 2026
4f6e57c
refactor: simplify forward authentication handling in UI and API
Siumauricio Jun 6, 2026
a3c8b3b
refactor: unify branch validation imports across provider components
Siumauricio Jun 6, 2026
931203a
refactor: remove obsolete SQL migration files and snapshots
Siumauricio Jun 6, 2026
35f452d
Merge branch 'canary' into feat/forward-auth-sso
Siumauricio Jun 6, 2026
1df6774
refactor: update forward authentication handling in domain schema and…
Siumauricio Jun 6, 2026
238bb2f
chore: remove PR quality workflow configuration
Siumauricio Jun 6, 2026
28673a6
Merge branch 'canary' into feat/forward-auth-sso
Siumauricio Jun 6, 2026
51b5af5
refactor: enhance forward authentication UI and API integration
Siumauricio Jun 6, 2026
aa545ec
feat: add SQL migration for lucky echo and update foreign key constra…
Siumauricio Jun 6, 2026
705ca54
refactor: improve path validation in Traefik configuration schema
Siumauricio Jun 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ describe("Host rule format regression tests", () => {
stripPath: false,
customEntrypoint: null,
middlewares: null,
forwardAuthEnabled: false,
};

describe("Host rule format validation", () => {
Expand Down
1 change: 1 addition & 0 deletions apps/dokploy/__test__/compose/domain/labels.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ describe("createDomainLabels", () => {
internalPath: "/",
stripPath: false,
middlewares: null,
forwardAuthEnabled: false,
};

it("should create basic labels for web entrypoint", async () => {
Expand Down
233 changes: 233 additions & 0 deletions apps/dokploy/__test__/traefik/forward-auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
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);
});
});
1 change: 1 addition & 0 deletions apps/dokploy/__test__/traefik/traefik.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ const baseDomain: Domain = {
internalPath: "/",
stripPath: false,
middlewares: null,
forwardAuthEnabled: false,
};

const baseRedirect: Redirect = {
Expand Down
Loading
Loading