fix: gate second factors after SSO/LDAP behind an admin setting

This commit is contained in:
LukeGus committed 2026-09-23 07:17:03 -05:00
1 parent f217d8db89
commit 95abe24798
13 files changed
+307 -5

No files matched your search

+15
View File
@@ -36,6 +36,21 @@ export function isPasswordLoginSettingOn(): boolean {
}
}
/**
* Off by default, matching 2.8: second factors only ran after password and
* other local form logins. On, they also run after external methods (SSO,
* LDAP), which needs the pending-cookie/temp_token step those methods'
* clients (Termix-Mobile included) may not handle yet.
*/
export function isSecondFactorAfterExternalLoginEnabled(): boolean {
try {
const value = getCurrentSettingValue("second_factor_after_external_login");
return value === "true";
} catch {
return false;
}
}
/**
* Whether anyone could still sign in without a password: trusted proxy auth,
* or a login method reporting an enabled instance (an SSO provider).
+2
View File
@@ -317,6 +317,7 @@ const oidcLoginMethod: LoginMethod = {
labelKey: "auth.loginWithSso",
icon: "key-round",
kind: "redirect",
external: true,
describe: async () =>
(await listPublicProviders(["oidc", "github", "google"])).map(
(provider) => ({
@@ -345,6 +346,7 @@ const ldapLoginMethod: LoginMethod = {
labelKey: "auth.loginWithLdap",
icon: "server",
kind: "form",
external: true,
describe: async () =>
(await listPublicProviders(["ldap"])).map((provider) => ({
id: String(provider.id),
+20 -2
View File
@@ -25,9 +25,13 @@ import {
createCurrentUserRepository,
} from "../database/repositories/factory.js";
import type { UserRecord } from "../database/repositories/user-repository.js";
import { ensureCoreLoginProviders } from "./core-auth.js";
import {
ensureCoreLoginProviders,
isSecondFactorAfterExternalLoginEnabled,
} from "./core-auth.js";
import { findOrProvisionExternalUser } from "./provisioning.js";
import {
getLoginMethod,
getSecondFactor,
listSecondFactors,
type SecondFactor,
@@ -112,6 +116,18 @@ export async function evaluateSecondFactors(
return { required: [...required.values()], blocked };
}
/**
* Whether second factors should even be asked for this login. Off, they only
* run for password and other local form logins, matching 2.8: the setting
* governs external methods (SSO, LDAP) alone.
*/
function shouldRunSecondFactors(methodId: string): boolean {
ensureCoreLoginProviders();
const method = getLoginMethod(methodId);
if (!method?.external) return true;
return isSecondFactorAfterExternalLoginEnabled();
}
async function isTrustedDevice(req: Request, userId: string): Promise<boolean> {
const deviceInfo = parseUserAgent(req);
const fingerprint = generateDeviceFingerprint(deviceInfo, getDeviceId(req));
@@ -221,7 +237,9 @@ export async function runLogin(
`${context.methodId}_role_shared_credentials`,
);
const factors = await evaluateSecondFactors(user.id);
const factors = shouldRunSecondFactors(context.methodId)
? await evaluateSecondFactors(user.id)
: { required: [], blocked: [] };
if (factors.blocked.length > 0) {
authLogger.warn("Login refused: an enrolled second factor is unavailable", {
operation: "second_factor_unavailable",