feat: add OIDC auto-provision setting independent of registration toggle (#795)

Allows OIDC-authenticated users to be auto-provisioned even when
"Allow new account registration" is disabled. Adds an admin UI toggle
and corresponding API endpoints (GET/PATCH /users/oidc-auto-provision).
Falls back to OIDC_ALLOW_REGISTRATION env var if the setting is unset.
This commit is contained in:
ZacharyZcR
2026-05-21 02:16:05 +08:00
committed by GitHub
parent ecb7f4f2c5
commit 21f24a8386
3 changed files with 116 additions and 5 deletions
+67 -4
View File
@@ -1166,11 +1166,28 @@ router.get("/oidc/callback", async (req, res) => {
}
}
const oidcAllowRegistration =
(process.env.OIDC_ALLOW_REGISTRATION || "").trim().toLowerCase() ===
"true";
let oidcAutoProvision = false;
try {
const oidcProvRow = db.$client
.prepare(
"SELECT value FROM settings WHERE key = 'oidc_auto_provision'",
)
.get();
if (oidcProvRow) {
oidcAutoProvision =
(oidcProvRow as Record<string, unknown>).value === "true";
}
} catch {
// fall through to env var check
}
if (!isFirstUser && !oidcAllowRegistration) {
if (!oidcAutoProvision) {
oidcAutoProvision =
(process.env.OIDC_ALLOW_REGISTRATION || "").trim().toLowerCase() ===
"true";
}
if (!isFirstUser && !oidcAutoProvision) {
try {
const regRow = db.$client
.prepare(
@@ -1924,6 +1941,52 @@ router.patch("/registration-allowed", authenticateJWT, async (req, res) => {
}
});
router.get("/oidc-auto-provision", async (_req, res) => {
try {
const row = db.$client
.prepare("SELECT value FROM settings WHERE key = 'oidc_auto_provision'")
.get();
res.json({
enabled: row
? (row as Record<string, unknown>).value === "true"
: false,
});
} catch (err) {
authLogger.error("Failed to get OIDC auto-provision setting", err);
res.status(500).json({ error: "Failed to get OIDC auto-provision setting" });
}
});
router.patch("/oidc-auto-provision", authenticateJWT, async (req, res) => {
const userId = (req as AuthenticatedRequest).userId;
try {
const user = await db.select().from(users).where(eq(users.id, userId));
if (!user || user.length === 0 || !user[0].isAdmin) {
return res.status(403).json({ error: "Not authorized" });
}
const { enabled } = req.body;
if (typeof enabled !== "boolean") {
return res.status(400).json({ error: "Invalid value for enabled" });
}
const existing = db.$client
.prepare("SELECT value FROM settings WHERE key = 'oidc_auto_provision'")
.get();
if (existing) {
db.$client
.prepare("UPDATE settings SET value = ? WHERE key = 'oidc_auto_provision'")
.run(enabled ? "true" : "false");
} else {
db.$client
.prepare("INSERT INTO settings (key, value) VALUES ('oidc_auto_provision', ?)")
.run(enabled ? "true" : "false");
}
res.json({ enabled });
} catch (err) {
authLogger.error("Failed to set OIDC auto-provision", err);
res.status(500).json({ error: "Failed to set OIDC auto-provision" });
}
});
/**
* @openapi
* /users/password-login-allowed:
+22
View File
@@ -3326,6 +3326,28 @@ export async function updateRegistrationAllowed(
}
}
export async function getOidcAutoProvision(): Promise<{ enabled: boolean }> {
try {
const response = await authApi.get("/users/oidc-auto-provision");
return response.data;
} catch (error) {
handleApiError(error, "check OIDC auto-provision status");
}
}
export async function updateOidcAutoProvision(
enabled: boolean,
): Promise<Record<string, unknown>> {
try {
const response = await authApi.patch("/users/oidc-auto-provision", {
enabled,
});
return response.data;
} catch (error) {
handleApiError(error, "update OIDC auto-provision");
}
}
export async function updatePasswordLoginAllowed(
allowed: boolean,
): Promise<{ allowed: boolean }> {
+27 -1
View File
@@ -31,6 +31,8 @@ import {
getAdminOIDCConfig,
updateOIDCConfig,
disableOIDCConfig,
getOidcAutoProvision,
updateOidcAutoProvision,
isElectron,
getUserRoles,
assignRoleToUser,
@@ -134,6 +136,7 @@ export function AdminSettingsPanel() {
const [logLevel, setLogLevel] = useState("info");
// OIDC state
const [oidcAutoProvision, setOidcAutoProvision] = useState(false);
const [oidcClientId, setOidcClientId] = useState("");
const [oidcClientSecret, setOidcClientSecret] = useState("");
const [oidcAuthUrl, setOidcAuthUrl] = useState("");
@@ -238,7 +241,7 @@ export function AdminSettingsPanel() {
async function loadGeneralSettings() {
try {
const [reg, pwLogin, pwReset, timeout, monitoring, level, guac] =
const [reg, pwLogin, pwReset, timeout, monitoring, level, guac, oidcProv] =
await Promise.allSettled([
getRegistrationAllowed(),
getPasswordLoginAllowed(),
@@ -247,11 +250,14 @@ export function AdminSettingsPanel() {
getGlobalMonitoringSettings(),
getLogLevel(),
getGuacamoleSettings(),
getOidcAutoProvision(),
]);
if (reg.status === "fulfilled") setAllowRegistration(reg.value.allowed);
if (pwLogin.status === "fulfilled")
setAllowPasswordLogin(pwLogin.value.allowed);
if (oidcProv.status === "fulfilled")
setOidcAutoProvision(oidcProv.value.enabled);
if (pwReset.status === "fulfilled") setAllowPasswordReset(pwReset.value);
if (timeout.status === "fulfilled")
setSessionTimeout(String(timeout.value.timeoutHours));
@@ -321,6 +327,17 @@ export function AdminSettingsPanel() {
}
}
async function handleToggleOidcAutoProvision() {
const newVal = !oidcAutoProvision;
setOidcAutoProvision(newVal);
try {
await updateOidcAutoProvision(newVal);
} catch {
setOidcAutoProvision(!newVal);
toast.error("Failed to update OIDC auto-provision setting");
}
}
async function handleTogglePasswordReset() {
const newVal = !allowPasswordReset;
setAllowPasswordReset(newVal);
@@ -700,6 +717,15 @@ export function AdminSettingsPanel() {
onToggle={handleTogglePasswordLogin}
/>
</SettingRow>
<SettingRow
label="OIDC Auto-Provision"
description="Auto-create accounts for OIDC users even when registration is disabled"
>
<AdminToggle
on={oidcAutoProvision}
onToggle={handleToggleOidcAutoProvision}
/>
</SettingRow>
<SettingRow
label="Allow Password Reset"
description="Reset code via Docker logs"