OIDC + SSO

This commit is contained in:
KingKDot
2026-07-08 20:34:25 -05:00
parent 97e9bf8f3d
commit faac5b6029
20 changed files with 1885 additions and 11 deletions
+190
View File
@@ -82,6 +82,28 @@ const tlsCertbotCertFileInput = document.getElementById("tls-certbot-cert-file")
const tlsCertbotKeyFileInput = document.getElementById("tls-certbot-key-file");
const tlsCertbotCaFileInput = document.getElementById("tls-certbot-ca-file");
const oidcForm = document.getElementById("oidc-form");
const oidcPermissionNote = document.getElementById("oidc-permission-note");
const oidcSaveBtn = document.getElementById("oidc-save-btn");
const oidcEnabledInput = document.getElementById("oidc-enabled");
const oidcLabelInput = document.getElementById("oidc-label");
const oidcIssuerInput = document.getElementById("oidc-issuer");
const oidcClientIdInput = document.getElementById("oidc-client-id");
const oidcClientSecretInput = document.getElementById("oidc-client-secret");
const oidcSecretHint = document.getElementById("oidc-secret-hint");
const oidcRedirectUriInput = document.getElementById("oidc-redirect-uri");
const oidcScopesInput = document.getElementById("oidc-scopes");
const oidcClientAuthMethodInput = document.getElementById("oidc-client-auth-method");
const oidcAutoProvisionInput = document.getElementById("oidc-auto-provision");
const oidcAllowEmailLinkInput = document.getElementById("oidc-allow-email-link");
const oidcDefaultRoleInput = document.getElementById("oidc-default-role");
const oidcAllowedDomainsInput = document.getElementById("oidc-allowed-domains");
const oidcAllowedEmailsInput = document.getElementById("oidc-allowed-emails");
const oidcGroupClaimInput = document.getElementById("oidc-group-claim");
const oidcAdminGroupsInput = document.getElementById("oidc-admin-groups");
const oidcOperatorGroupsInput = document.getElementById("oidc-operator-groups");
const oidcViewerGroupsInput = document.getElementById("oidc-viewer-groups");
const appearanceForm = document.getElementById("appearance-form");
const appearancePermissionNote = document.getElementById("appearance-permission-note");
const appearanceSaveBtn = document.getElementById("appearance-save-btn");
@@ -99,6 +121,7 @@ const wipeOfflineMessage = document.getElementById("wipe-offline-message");
let currentUser = null;
let securityConfig = null;
let tlsConfig = null;
let oidcConfig = null;
function showMessage(text, type = "ok") {
if (!messageEl) return;
@@ -212,6 +235,7 @@ async function renderPermissionsOverview() {
perms: [
"system:security",
"system:tls",
"system:oidc",
"system:registration",
"system:notifications",
"system:chat",
@@ -425,6 +449,94 @@ async function loadTlsSettings() {
setTlsFormDisabled(false);
}
function listToCsv(value) {
return Array.isArray(value) ? value.join(", ") : "";
}
function csvToList(value) {
return String(value || "")
.split(",")
.map((item) => item.trim())
.filter(Boolean);
}
function setOidcFormDisabled(disabled) {
const controls = [
oidcEnabledInput,
oidcLabelInput,
oidcIssuerInput,
oidcClientIdInput,
oidcClientSecretInput,
oidcRedirectUriInput,
oidcScopesInput,
oidcClientAuthMethodInput,
oidcAutoProvisionInput,
oidcAllowEmailLinkInput,
oidcDefaultRoleInput,
oidcAllowedDomainsInput,
oidcAllowedEmailsInput,
oidcGroupClaimInput,
oidcAdminGroupsInput,
oidcOperatorGroupsInput,
oidcViewerGroupsInput,
oidcSaveBtn,
];
for (const control of controls) {
if (!control) continue;
control.disabled = disabled;
}
}
function applyOidcForm() {
const cfg = oidcConfig || {};
if (oidcEnabledInput) oidcEnabledInput.checked = Boolean(cfg.enabled);
if (oidcLabelInput) oidcLabelInput.value = cfg.label || "Single sign-on";
if (oidcIssuerInput) oidcIssuerInput.value = cfg.issuer || "";
if (oidcClientIdInput) oidcClientIdInput.value = cfg.clientId || "";
if (oidcClientSecretInput) oidcClientSecretInput.value = "";
if (oidcSecretHint) {
oidcSecretHint.textContent = cfg.clientSecretSet
? "A client secret is saved. Leave this blank to keep it."
: "No client secret is currently saved.";
}
if (oidcRedirectUriInput) oidcRedirectUriInput.value = cfg.redirectUri || "";
if (oidcScopesInput) oidcScopesInput.value = listToCsv(cfg.scopes || ["openid", "profile", "email"]);
if (oidcClientAuthMethodInput) oidcClientAuthMethodInput.value = cfg.clientAuthMethod || "client_secret_post";
if (oidcAutoProvisionInput) oidcAutoProvisionInput.checked = cfg.autoProvision !== false;
if (oidcAllowEmailLinkInput) oidcAllowEmailLinkInput.checked = Boolean(cfg.allowEmailLink);
if (oidcDefaultRoleInput) oidcDefaultRoleInput.value = cfg.defaultRole || "viewer";
if (oidcAllowedDomainsInput) oidcAllowedDomainsInput.value = listToCsv(cfg.allowedDomains);
if (oidcAllowedEmailsInput) oidcAllowedEmailsInput.value = listToCsv(cfg.allowedEmails);
if (oidcGroupClaimInput) oidcGroupClaimInput.value = cfg.groupClaim || "groups";
if (oidcAdminGroupsInput) oidcAdminGroupsInput.value = listToCsv(cfg.adminGroups);
if (oidcOperatorGroupsInput) oidcOperatorGroupsInput.value = listToCsv(cfg.operatorGroups);
if (oidcViewerGroupsInput) oidcViewerGroupsInput.value = listToCsv(cfg.viewerGroups);
}
async function loadOidcSettings() {
if (!currentUser) return;
if (!userHas("system:oidc")) {
if (oidcPermissionNote) oidcPermissionNote.classList.remove("hidden");
setOidcFormDisabled(true);
return;
}
if (oidcPermissionNote) oidcPermissionNote.classList.add("hidden");
const res = await fetch("/api/settings/oidc", { credentials: "include" });
if (!res.ok) {
showMessage("Failed to load OIDC settings.", "error");
setOidcFormDisabled(true);
return;
}
const data = await res.json().catch(() => ({}));
oidcConfig = data.oidc || null;
applyOidcForm();
setOidcFormDisabled(false);
}
function updateNavLayoutButtons(mode, sidebarBtn, topbarBtn) {
const active = ["bg-indigo-600/80", "border-indigo-500", "text-white"];
const inactive = ["bg-slate-800", "border-slate-700", "text-slate-400", "hover:bg-slate-700", "hover:text-slate-200"];
@@ -723,6 +835,82 @@ async function saveTlsSettings(event) {
showMessage("TLS settings updated. Restart server to apply.");
}
async function saveOidcSettings(event) {
event.preventDefault();
if (!requireUiPermission("system:oidc", "OIDC settings permission required.")) {
return;
}
const payload = {
enabled: !!oidcEnabledInput?.checked,
label: String(oidcLabelInput?.value || "").trim(),
issuer: String(oidcIssuerInput?.value || "").trim(),
clientId: String(oidcClientIdInput?.value || "").trim(),
redirectUri: String(oidcRedirectUriInput?.value || "").trim(),
scopes: csvToList(oidcScopesInput?.value),
clientAuthMethod: oidcClientAuthMethodInput?.value || "client_secret_post",
autoProvision: !!oidcAutoProvisionInput?.checked,
allowEmailLink: !!oidcAllowEmailLinkInput?.checked,
defaultRole: oidcDefaultRoleInput?.value || "viewer",
allowedDomains: csvToList(oidcAllowedDomainsInput?.value),
allowedEmails: csvToList(oidcAllowedEmailsInput?.value),
groupClaim: String(oidcGroupClaimInput?.value || "").trim(),
adminGroups: csvToList(oidcAdminGroupsInput?.value),
operatorGroups: csvToList(oidcOperatorGroupsInput?.value),
viewerGroups: csvToList(oidcViewerGroupsInput?.value),
};
const clientSecret = String(oidcClientSecretInput?.value || "");
if (clientSecret) {
payload.clientSecret = clientSecret;
}
if (payload.enabled) {
if (!payload.issuer) {
showMessage("OIDC issuer URL is required when OIDC is enabled.", "error");
oidcIssuerInput?.focus();
return;
}
if (!payload.redirectUri) {
showMessage("OIDC redirect URI is required when OIDC is enabled.", "error");
oidcRedirectUriInput?.focus();
return;
}
if (!payload.clientId) {
showMessage("OIDC client ID is required when OIDC is enabled.", "error");
oidcClientIdInput?.focus();
return;
}
if (!payload.scopes.length || !payload.scopes.includes("openid")) {
showMessage("OIDC scopes must include openid.", "error");
oidcScopesInput?.focus();
return;
}
if (payload.clientAuthMethod !== "none" && !clientSecret && !oidcConfig?.clientSecretSet) {
showMessage("OIDC client secret is required for the selected client auth method.", "error");
oidcClientSecretInput?.focus();
return;
}
}
const res = await fetch("/api/settings/oidc", {
method: "PUT",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify(payload),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
showMessage(data.error || "Failed to save OIDC settings.", "error");
return;
}
oidcConfig = data.oidc || payload;
applyOidcForm();
showMessage("OIDC settings saved. Environment variables still take priority after restart.");
}
async function runCertbotAutoSetup() {
if (!requireUiPermission("system:tls", "TLS settings permission required.")) {
return;
@@ -1448,6 +1636,7 @@ async function importSettings(event) {
await loadSecurityPolicy();
await loadTlsSettings();
await loadOidcSettings();
await loadAppearanceSettings();
} catch (error) {
showExportImportMessage(`Import failed: ${String(error?.message || error)}`, "error");
@@ -2429,6 +2618,7 @@ async function init() {
securityForm.addEventListener("submit", saveSecurityPolicy);
tlsForm.addEventListener("submit", saveTlsSettings);
tlsCertbotAutoBtn.addEventListener("click", runCertbotAutoSetup);
if (oidcForm) oidcForm.addEventListener("submit", saveOidcSettings);
if (appearanceForm) appearanceForm.addEventListener("submit", saveAppearanceSettings);
if (chatSettingsForm) chatSettingsForm.addEventListener("submit", saveChatSettings);
if (inputArchiveUserForm) inputArchiveUserForm.addEventListener("submit", saveInputArchivePreference);