feat(auth): add native SAML 2.0 SSO integration

Add SAML 2.0 as a second SSO protocol alongside OIDC under a unified
authMode/ssoType model. SP flows via @node-saml/node-saml: AuthnRequest
generation, ACS POST assertion handling, SP metadata export, and admin
config test endpoint. Replay-protected via saml_state cookie (httpOnly,
SameSite=Lax) matched against InResponseTo; wantAssertionsSigned enforced.

- src/lib/auth/saml.js: SAML instance builder, X.509 cert formatter, claim pickers
- 4 routes under src/app/api/auth/saml/: start, acs, metadata, test
- settingsRepo: ssoType + saml* defaults; login/status routes dispatch by type
- profile page: SSO protocol switcher, IdP metadata XML + cert uploaders
- login page: dynamic SAML sign-in button; Header: SAML user badge
This commit is contained in:
Duc Nguyen authored and decolua committed 2026-08-13 17:56:34 +07:00
1 parent e02bde4a70
commit 65197ad11c
17 files changed
+1396 -166

No files matched your search

+25 -2
View File
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import { getSettings } from "@/lib/localDb";
import { isOidcConfigured } from "@/lib/auth/oidc";
import { isSamlConfigured } from "@/lib/auth/saml.js";
import { getDashboardAuthSession } from "@/lib/auth/dashboardSession";
export async function GET() {
@@ -11,16 +12,29 @@ export async function GET() {
const session = await getDashboardAuthSession(cookieStore.get("auth_token")?.value);
const requireLogin = settings.requireLogin !== false;
const authMode = settings.authMode || "password";
const ssoType = settings.ssoType || "oidc";
const oidcName = String(session?.oidcName || "").trim();
const oidcEmail = String(session?.oidcEmail || "").trim();
const displayName = oidcName || oidcEmail || (session?.oidc ? "OIDC user" : "Password user");
const loginMethod = session?.oidc ? "OIDC" : "Password";
const samlName = String(session?.samlName || "").trim();
const samlEmail = String(session?.samlEmail || "").trim();
const displayName =
samlName ||
samlEmail ||
oidcName ||
oidcEmail ||
(session?.saml ? "SAML user" : session?.oidc ? "OIDC user" : "Password user");
const loginMethod = session?.saml ? "SAML" : session?.oidc ? "OIDC" : "Password";
return NextResponse.json({
requireLogin,
authMode,
ssoType,
oidcConfigured: isOidcConfigured(settings),
oidcLoginLabel: (settings.oidcLoginLabel || "Sign in with OIDC").trim() || "Sign in with OIDC",
samlConfigured: isSamlConfigured(settings),
samlLoginLabel: (settings.samlLoginLabel || "Sign in with SAML SSO").trim() || "Sign in with SAML SSO",
hasPassword: !!settings.password,
displayName,
loginMethod,
@@ -28,13 +42,19 @@ export async function GET() {
oidcName: oidcName || null,
oidcEmail: oidcEmail || null,
oidcLogin: !!session?.oidc,
samlName: samlName || null,
samlEmail: samlEmail || null,
samlLogin: !!session?.saml,
});
} catch {
return NextResponse.json({
requireLogin: true,
authMode: "password",
ssoType: "oidc",
oidcConfigured: false,
oidcLoginLabel: "Sign in with OIDC",
samlConfigured: false,
samlLoginLabel: "Sign in with SAML SSO",
hasPassword: false,
displayName: "Password user",
loginMethod: "Password",
@@ -42,6 +62,9 @@ export async function GET() {
oidcName: null,
oidcEmail: null,
oidcLogin: false,
samlName: null,
samlEmail: null,
samlLogin: false,
});
}
}