From 713b36662ca286e85ef5f9fb5a53f2edb0dd46a2 Mon Sep 17 00:00:00 2001 From: purian23 Date: Mon, 9 Mar 2026 14:37:34 -0400 Subject: [PATCH] (greeter): Enhance external auth handling in package/DEV mode - add PAM manager hints for logging --- core/cmd/dms/commands_greeter.go | 89 +++++++++++++++++--- core/internal/greeter/installer.go | 58 ++++++++++++- quickshell/Modules/Greetd/GreeterContent.qml | 65 +++++++++----- 3 files changed, 175 insertions(+), 37 deletions(-) diff --git a/core/cmd/dms/commands_greeter.go b/core/cmd/dms/commands_greeter.go index b0ee8146..261e22d6 100644 --- a/core/cmd/dms/commands_greeter.go +++ b/core/cmd/dms/commands_greeter.go @@ -1249,7 +1249,17 @@ func extractGreeterWrapperFromCommand(command string) string { if len(tokens) == 0 { return "" } - return strings.Trim(tokens[0], "\"") + wrapper := strings.Trim(tokens[0], "\"") + if wrapper == "" { + return "" + } + if len(tokens) > 1 { + next := strings.Trim(tokens[1], "\"") + if next != "" && (filepath.Base(wrapper) == "bash" || filepath.Base(wrapper) == "sh") && strings.Contains(filepath.Base(next), "dms-greeter") { + return fmt.Sprintf("%s (script: %s)", wrapper, next) + } + } + return wrapper } func extractGreeterPathOverrideFromCommand(command string) string { @@ -1335,6 +1345,26 @@ func packageInstallHint() string { } } +func systemPamManagerRemediationHint() string { + osInfo, err := distros.GetOSInfo() + if err != nil { + return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login." + } + config, exists := distros.Registry[osInfo.Distribution.ID] + if !exists { + return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login." + } + + switch config.Family { + case distros.FamilyFedora: + return "Disable it in authselect to force password-only greeter login." + case distros.FamilyDebian, distros.FamilyUbuntu: + return "Disable it in pam-auth-update to force password-only greeter login." + default: + return "Disable it in your distro PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login." + } +} + func isPackageOnlyGreeterDistro() bool { osInfo, err := distros.GetOSInfo() if err != nil { @@ -1568,22 +1598,55 @@ func checkGreeterStatus() error { fmt.Println(" ⚠ Legacy unmanaged DMS PAM lines detected. Run 'dms greeter sync' to normalize.") allGood = false } + enableFprintToggle, enableU2fToggle := false, false + if enableFprint, enableU2f, settingsErr := greeter.ReadGreeterAuthToggles(homeDir); settingsErr == nil { + enableFprintToggle = enableFprint + enableU2fToggle = enableU2f + } else { + fmt.Printf(" ℹ Could not read greeter auth toggles from settings: %v\n", settingsErr) + } + includedFprintFile := greeter.DetectIncludedPamModule(string(pamData), "pam_fprintd.so") - showIncludedFprintNotice := false - if includedFprintFile != "" { - if enableFprint, _, settingsErr := greeter.ReadGreeterAuthToggles(homeDir); settingsErr == nil && enableFprint { - showIncludedFprintNotice = greeter.FingerprintAuthAvailableForCurrentUser() + includedU2fFile := greeter.DetectIncludedPamModule(string(pamData), "pam_u2f.so") + fprintAvailableForCurrentUser := greeter.FingerprintAuthAvailableForCurrentUser() + + if managedFprint && includedFprintFile != "" { + fmt.Printf(" ⚠ pam_fprintd found in both DMS managed block and %s.\n", includedFprintFile) + fmt.Println(" Double fingerprint auth detected — run 'dms greeter sync' to resolve.") + allGood = false + } + if managedU2f && includedU2fFile != "" { + fmt.Printf(" ⚠ pam_u2f found in both DMS managed block and %s.\n", includedU2fFile) + fmt.Println(" Double security-key auth detected — run 'dms greeter sync' to resolve.") + allGood = false + } + + if includedFprintFile != "" && !managedFprint { + if enableFprintToggle { + fmt.Printf(" ℹ Fingerprint auth is enabled via included %s.\n", includedFprintFile) + if fprintAvailableForCurrentUser { + fmt.Println(" DMS toggle is enabled, and effective auth is coming from the included PAM stack.") + } else { + fmt.Println(" No enrolled fingerprints detected for the current user; password auth remains the effective path.") + } + } else { + if fprintAvailableForCurrentUser { + fmt.Printf(" ⚠ Fingerprint auth is active via included %s while DMS fingerprint toggle is off.\n", includedFprintFile) + fmt.Printf(" %s\n", systemPamManagerRemediationHint()) + } else { + fmt.Printf(" ℹ pam_fprintd is present via included %s, but no enrolled fingerprints were detected for user %s.\n", includedFprintFile, currentUser.Username) + fmt.Println(" Password auth remains the effective login path.") + } } } - if managedFprint { - if includedFprintFile != "" { - fmt.Printf(" ⚠ pam_fprintd found in both DMS managed block and %s.\n", includedFprintFile) - fmt.Println(" Double fingerprint auth detected — run 'dms greeter sync' to resolve.") - allGood = false + if includedU2fFile != "" && !managedU2f { + if enableU2fToggle { + fmt.Printf(" ℹ Security-key auth is enabled via included %s.\n", includedU2fFile) + fmt.Println(" DMS toggle is enabled, but effective auth is coming from the included PAM stack.") + } else { + fmt.Printf(" ⚠ Security-key auth is active via included %s while DMS security-key toggle is off.\n", includedU2fFile) + fmt.Printf(" %s\n", systemPamManagerRemediationHint()) } - } else if includedFprintFile != "" && showIncludedFprintNotice { - fmt.Printf(" ℹ Fingerprint auth is enabled via included %s.\n", includedFprintFile) - fmt.Println(" The DMS toggle only controls the managed block; disable fingerprint in authselect/pam-auth-update for password-only greeter login.") } } diff --git a/core/internal/greeter/installer.go b/core/internal/greeter/installer.go index 49b0986c..091cb364 100644 --- a/core/internal/greeter/installer.go +++ b/core/internal/greeter/installer.go @@ -1424,9 +1424,30 @@ func FingerprintAuthAvailableForCurrentUser() bool { return FingerprintAuthAvailableForUser(username) } +func pamManagerHintForCurrentDistro() string { + osInfo, err := distros.GetOSInfo() + if err != nil { + return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login." + } + config, exists := distros.Registry[osInfo.Distribution.ID] + if !exists { + return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login." + } + + switch config.Family { + case distros.FamilyFedora: + return "Disable it in authselect to force password-only greeter login." + case distros.FamilyDebian, distros.FamilyUbuntu: + return "Disable it in pam-auth-update to force password-only greeter login." + default: + return "Disable it in your distro PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login." + } +} + func syncGreeterPamConfig(homeDir string, logFunc func(string), sudoPassword string, forceAuth bool) error { var wantFprint, wantU2f bool fprintToggleEnabled := forceAuth + u2fToggleEnabled := forceAuth if forceAuth { wantFprint = pamModuleExists("pam_fprintd.so") wantU2f = pamModuleExists("pam_u2f.so") @@ -1436,6 +1457,7 @@ func syncGreeterPamConfig(homeDir string, logFunc func(string), sudoPassword str return err } fprintToggleEnabled = settings.GreeterEnableFprint + u2fToggleEnabled = settings.GreeterEnableU2f fprintModule := pamModuleExists("pam_fprintd.so") u2fModule := pamModuleExists("pam_u2f.so") wantFprint = settings.GreeterEnableFprint && fprintModule @@ -1464,14 +1486,42 @@ func syncGreeterPamConfig(homeDir string, logFunc func(string), sudoPassword str content, _ = stripLegacyGreeterPamLines(content) includedFprintFile := DetectIncludedPamModule(content, "pam_fprintd.so") + includedU2fFile := DetectIncludedPamModule(content, "pam_u2f.so") + fprintAvailableForCurrentUser := FingerprintAuthAvailableForCurrentUser() if wantFprint && includedFprintFile != "" { logFunc("⚠ pam_fprintd already present in included " + includedFprintFile + " (managed by authselect/pam-auth-update). Skipping DMS fprint block to avoid double-fingerprint auth.") wantFprint = false } - showIncludedFprintNotice := fprintToggleEnabled && FingerprintAuthAvailableForCurrentUser() - if !wantFprint && includedFprintFile != "" && showIncludedFprintNotice { - logFunc("ℹ Fingerprint auth is still enabled via included " + includedFprintFile + ".") - logFunc(" Disable fingerprint in your system PAM manager (authselect/pam-auth-update) to force password-only greeter login.") + if wantU2f && includedU2fFile != "" { + logFunc("⚠ pam_u2f already present in included " + includedU2fFile + " (managed by authselect/pam-auth-update). Skipping DMS U2F block to avoid double security-key auth.") + wantU2f = false + } + if !wantFprint && includedFprintFile != "" { + if fprintToggleEnabled { + logFunc("ℹ Fingerprint auth is still enabled via included " + includedFprintFile + ".") + if fprintAvailableForCurrentUser { + logFunc(" DMS toggle is enabled, and effective auth is provided by the included PAM stack.") + } else { + logFunc(" No enrolled fingerprints detected for the current user; password auth remains the effective path.") + } + } else { + if fprintAvailableForCurrentUser { + logFunc("⚠ Fingerprint auth is active via included " + includedFprintFile + " while DMS fingerprint toggle is off.") + logFunc(" " + pamManagerHintForCurrentDistro()) + } else { + logFunc("ℹ pam_fprintd is present via included " + includedFprintFile + ", but no enrolled fingerprints were detected for the current user.") + logFunc(" Password auth remains the effective login path.") + } + } + } + if !wantU2f && includedU2fFile != "" { + if u2fToggleEnabled { + logFunc("ℹ Security-key auth is still enabled via included " + includedU2fFile + ".") + logFunc(" DMS toggle is enabled, but effective auth is provided by the included PAM stack.") + } else { + logFunc("⚠ Security-key auth is active via included " + includedU2fFile + " while DMS security-key toggle is off.") + logFunc(" " + pamManagerHintForCurrentDistro()) + } } if wantFprint || wantU2f { diff --git a/quickshell/Modules/Greetd/GreeterContent.qml b/quickshell/Modules/Greetd/GreeterContent.qml index 031ee0ac..e7dafa20 100644 --- a/quickshell/Modules/Greetd/GreeterContent.qml +++ b/quickshell/Modules/Greetd/GreeterContent.qml @@ -50,9 +50,13 @@ Item { property string faillockConfigText: "" property bool greeterWallpaperOverrideExists: false property string externalAuthAutoStartedForUser: "" + property int passwordSessionTransitionRetryCount: 0 + property int maxPasswordSessionTransitionRetries: 2 readonly property bool greeterPamHasFprint: pamModuleEnabled(greetdPamText, "pam_fprintd") || (greetdPamText.includes("system-auth") && pamModuleEnabled(systemAuthPamText, "pam_fprintd")) || (greetdPamText.includes("common-auth") && pamModuleEnabled(commonAuthPamText, "pam_fprintd")) || (greetdPamText.includes("password-auth") && pamModuleEnabled(passwordAuthPamText, "pam_fprintd")) readonly property bool greeterPamHasU2f: pamModuleEnabled(greetdPamText, "pam_u2f") || (greetdPamText.includes("system-auth") && pamModuleEnabled(systemAuthPamText, "pam_u2f")) || (greetdPamText.includes("common-auth") && pamModuleEnabled(commonAuthPamText, "pam_u2f")) || (greetdPamText.includes("password-auth") && pamModuleEnabled(passwordAuthPamText, "pam_u2f")) - readonly property bool greeterExternalAuthAvailable: (greeterPamHasFprint && GreetdSettings.greeterEnableFprint) || (greeterPamHasU2f && GreetdSettings.greeterEnableU2f) + readonly property bool greeterExternalAuthCapable: greeterPamHasFprint || greeterPamHasU2f + readonly property bool greeterExternalAuthEnabledByToggle: (greeterPamHasFprint && GreetdSettings.greeterEnableFprint) || (greeterPamHasU2f && GreetdSettings.greeterEnableU2f) + readonly property bool greeterExternalAuthAvailable: greeterExternalAuthCapable function initWeatherService() { if (weatherInitialized) @@ -208,6 +212,13 @@ Item { authFeedbackMessage = ""; } + function resetPasswordSessionTransition(clearSubmitRequest) { + cancelingExternalAuthForPassword = false; + passwordSessionTransitionRetryCount = 0; + if (clearSubmitRequest) + passwordSubmitRequested = false; + } + Connections { target: GreetdSettings function onSettingsLoadedChanged() { @@ -348,8 +359,7 @@ Item { PortalService.getGreeterUserProfileImage(user); GreeterState.passwordBuffer = ""; pendingPasswordResponse = false; - passwordSubmitRequested = false; - cancelingExternalAuthForPassword = false; + resetPasswordSessionTransition(true); maybeAutoStartExternalAuth(); } @@ -357,8 +367,7 @@ Item { if (!GreeterState.passwordBuffer || GreeterState.passwordBuffer.length === 0) return false; pendingPasswordResponse = false; - passwordSubmitRequested = false; - cancelingExternalAuthForPassword = false; + resetPasswordSessionTransition(true); awaitingExternalAuth = false; authTimeout.interval = defaultAuthTimeoutMs; authTimeout.restart(); @@ -369,9 +378,24 @@ Item { } function requestPasswordSessionTransition() { + if (!GreeterState.passwordBuffer || GreeterState.passwordBuffer.length === 0) + return; if (cancelingExternalAuthForPassword) return; + if (passwordSessionTransitionRetryCount >= maxPasswordSessionTransitionRetries) { + pendingPasswordResponse = false; + awaitingExternalAuth = false; + authTimeout.interval = defaultAuthTimeoutMs; + authTimeout.stop(); + resetPasswordSessionTransition(true); + GreeterState.pamState = "error"; + authFeedbackMessage = currentAuthMessage(); + placeholderDelay.restart(); + Greetd.cancelSession(); + return; + } cancelingExternalAuthForPassword = true; + passwordSessionTransitionRetryCount = passwordSessionTransitionRetryCount + 1; awaitingExternalAuth = false; pendingPasswordResponse = false; authTimeout.interval = defaultAuthTimeoutMs; @@ -390,6 +414,7 @@ Item { submitBufferedPassword(); else if (awaitingExternalAuth && hasPasswordBuffer) { passwordSubmitRequested = true; + requestPasswordSessionTransition(); } else if (hasPasswordBuffer) passwordSubmitRequested = true; return; @@ -399,11 +424,11 @@ Item { passwordSubmitRequested = true; return; } - if (!hasPasswordBuffer && !root.greeterExternalAuthAvailable) + if (!hasPasswordBuffer && !root.greeterExternalAuthEnabledByToggle) return; pendingPasswordResponse = false; passwordSubmitRequested = hasPasswordBuffer; - awaitingExternalAuth = !hasPasswordBuffer && root.greeterExternalAuthAvailable; + awaitingExternalAuth = !hasPasswordBuffer && root.greeterExternalAuthEnabledByToggle; authTimeout.interval = awaitingExternalAuth ? externalAuthTimeoutMs : defaultAuthTimeoutMs; authTimeout.restart(); Greetd.createSession(GreeterState.username); @@ -412,7 +437,7 @@ Item { function maybeAutoStartExternalAuth() { if (!GreeterState.showPasswordInput || !GreeterState.username) return; - if (!root.greeterExternalAuthAvailable) + if (!root.greeterExternalAuthEnabledByToggle) return; if (GreeterState.unlocking || Greetd.state !== GreetdState.Inactive) return; @@ -933,7 +958,7 @@ Item { anchors.verticalCenter: parent.verticalCenter iconName: root.greeterPamHasFprint ? "fingerprint" : "key" buttonSize: 32 - visible: GreeterState.showPasswordInput && root.greeterExternalAuthAvailable && GreeterState.passwordBuffer.length === 0 && (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking + visible: GreeterState.showPasswordInput && root.greeterExternalAuthEnabledByToggle && GreeterState.passwordBuffer.length === 0 && (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking enabled: visible onClicked: root.startAuthSession() } @@ -1559,6 +1584,7 @@ Item { function onAuthMessage(message, error, responseRequired, echoResponse) { if (responseRequired) { cancelingExternalAuthForPassword = false; + passwordSessionTransitionRetryCount = 0; awaitingExternalAuth = false; authTimeout.interval = defaultAuthTimeoutMs; authTimeout.restart(); @@ -1568,6 +1594,10 @@ Item { return; } pendingPasswordResponse = false; + if (passwordSubmitRequested && GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0 && awaitingExternalAuth && !cancelingExternalAuthForPassword) { + requestPasswordSessionTransition(); + return; + } if (!passwordSubmitRequested) awaitingExternalAuth = root.isExternalAuthPrompt(message, responseRequired); authTimeout.interval = awaitingExternalAuth ? externalAuthTimeoutMs : defaultAuthTimeoutMs; @@ -1587,15 +1617,14 @@ Item { Qt.callLater(root.startAuthSession); return; } - passwordSubmitRequested = false; + resetPasswordSessionTransition(true); } } function onReadyToLaunch() { awaitingExternalAuth = false; pendingPasswordResponse = false; - passwordSubmitRequested = false; - cancelingExternalAuthForPassword = false; + resetPasswordSessionTransition(true); authTimeout.interval = defaultAuthTimeoutMs; authTimeout.stop(); passwordFailureCount = 0; @@ -1629,8 +1658,7 @@ Item { function onAuthFailure(message) { awaitingExternalAuth = false; pendingPasswordResponse = false; - passwordSubmitRequested = false; - cancelingExternalAuthForPassword = false; + resetPasswordSessionTransition(true); authTimeout.interval = defaultAuthTimeoutMs; authTimeout.stop(); launchTimeout.stop(); @@ -1651,8 +1679,7 @@ Item { function onError(error) { awaitingExternalAuth = false; pendingPasswordResponse = false; - passwordSubmitRequested = false; - cancelingExternalAuthForPassword = false; + resetPasswordSessionTransition(true); authTimeout.interval = defaultAuthTimeoutMs; authTimeout.stop(); launchTimeout.stop(); @@ -1688,8 +1715,7 @@ Item { return; awaitingExternalAuth = false; pendingPasswordResponse = false; - passwordSubmitRequested = false; - cancelingExternalAuthForPassword = false; + resetPasswordSessionTransition(true); authTimeout.interval = defaultAuthTimeoutMs; GreeterState.pamState = "error"; authFeedbackMessage = currentAuthMessage(); @@ -1707,8 +1733,7 @@ Item { if (!GreeterState.unlocking) return; pendingPasswordResponse = false; - passwordSubmitRequested = false; - cancelingExternalAuthForPassword = false; + resetPasswordSessionTransition(true); GreeterState.unlocking = false; GreeterState.pamState = "error"; authFeedbackMessage = currentAuthMessage();