diff --git a/assets/example.conf b/assets/example.conf index 2cad488d..288aead5 100644 --- a/assets/example.conf +++ b/assets/example.conf @@ -15,6 +15,21 @@ general { hide_cursor = false } +# auth:pam:retry_on_error — when PAM returns a transient system error (e.g. +# SSSD's krb5_child process was killed by its hardcoded 300-second keep-alive +# timer before the user submitted their password), hyprlock will automatically +# retry authentication once after retry_delay milliseconds using the same +# credentials. This avoids having to enter the password twice after long idle +# periods on systems using Kerberos/SSSD authentication. +# Note: only PAM_SYSTEM_ERR triggers a retry; PAM_AUTH_ERR (wrong password) +# is never retried. +# auth { +# pam { +# retry_on_error = true +# retry_delay = 3000 # in milliseconds +# } +# } + # uncomment to enable fingerprint authentication # auth { # fingerprint { diff --git a/src/auth/Pam.cpp b/src/auth/Pam.cpp index 9cd16c92..3e0a535e 100644 --- a/src/auth/Pam.cpp +++ b/src/auth/Pam.cpp @@ -46,7 +46,10 @@ int conv(int num_msg, const struct pam_message** msg, struct pam_response** resp CONVERSATIONSTATE->prompt = PROMPT; g_pHyprlock->enqueueForceUpdateTimers(); - CONVERSATIONSTATE->waitForInput(); + // On auto-retry after a transient system error, the password is + // already set — skip blocking and reuse it directly. + if (!CONVERSATIONSTATE->autoRetry) + CONVERSATIONSTATE->waitForInput(); } // Needed for unlocks via SIGUSR1 @@ -77,8 +80,12 @@ int conv(int num_msg, const struct pam_message** msg, struct pam_response** resp } CPam::CPam() { - static const auto PAMMODULE = g_pConfigManager->getValue("auth:pam:module"); - m_sPamModule = *PAMMODULE; + static const auto PAMMODULE = g_pConfigManager->getValue("auth:pam:module"); + static const auto RETRYONERROR = g_pConfigManager->getValue("auth:pam:retry_on_error"); + static const auto RETRYDELAY = g_pConfigManager->getValue("auth:pam:retry_delay"); + m_sPamModule = *PAMMODULE; + m_bRetryOnError = *RETRYONERROR; + m_iRetryDelayMs = *RETRYDELAY; if (!std::filesystem::exists(std::filesystem::path("/etc/pam.d/") / m_sPamModule)) { Log::logger->log(Log::ERR, R"(Pam module "/etc/pam.d/{}" does not exist! Falling back to "/etc/pam.d/su")", m_sPamModule); @@ -142,6 +149,31 @@ bool CPam::auth() { m_sConversationState.waitingForPamAuth = false; + // PAM_SYSTEM_ERR (4) means an SSSD/PAM infrastructure transient failure, + // not a wrong password. Retry once after a short delay so that the user + // never has to type their password twice due to e.g. a dead krb5_child. + if (m_bRetryOnError && ret == PAM_SYSTEM_ERR && !m_sConversationState.input.empty() && + !m_sConversationState.terminateRequested && !g_pHyprlock->isFadingOutOrTerminating()) { + Log::logger->log(Log::INFO, "auth: PAM_SYSTEM_ERR — retrying in {}ms with same credentials", m_iRetryDelayMs); + std::this_thread::sleep_for(std::chrono::milliseconds(m_iRetryDelayMs)); + + if (!m_sConversationState.terminateRequested && !g_pHyprlock->isFadingOutOrTerminating()) { + m_sConversationState.autoRetry = true; + m_sConversationState.failTextFromPam = false; + m_sConversationState.waitingForPamAuth = true; + + pam_handle_t* retryHandle = nullptr; + ret = pam_start(m_sPamModule.c_str(), m_username.c_str(), &localConv, &retryHandle); + if (ret == PAM_SUCCESS) + ret = pam_authenticate(retryHandle, 0); + pam_end(retryHandle, ret); + retryHandle = nullptr; + + m_sConversationState.autoRetry = false; + m_sConversationState.waitingForPamAuth = false; + } + } + if (ret != PAM_SUCCESS) { if (!m_sConversationState.failTextFromPam) m_sConversationState.failText = ret == PAM_AUTH_ERR ? "Authentication failed" : "pam_authenticate failed"; @@ -205,5 +237,6 @@ void CPam::resetConversation() { m_sConversationState.waitingForPamAuth = false; m_sConversationState.inputRequested = false; m_sConversationState.failTextFromPam = false; + m_sConversationState.autoRetry = false; m_bBlockInput = false; } diff --git a/src/auth/Pam.hpp b/src/auth/Pam.hpp index 013031ca..6a5b7e62 100644 --- a/src/auth/Pam.hpp +++ b/src/auth/Pam.hpp @@ -23,6 +23,7 @@ class CPam : public IAuthImplementation { bool waitingForPamAuth = false; bool inputRequested = false; bool failTextFromPam = false; + bool autoRetry = false; std::atomic_bool terminateRequested = false; std::function waitForInput = []() {}; }; @@ -46,7 +47,9 @@ class CPam : public IAuthImplementation { std::thread m_thread; SPamConversationState m_sConversationState; - bool m_bBlockInput = false; + bool m_bBlockInput = false; + bool m_bRetryOnError = true; + int m_iRetryDelayMs = 3000; std::string m_sPamModule = ""; std::string m_username = ""; diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp index 7eef5f70..ca556387 100644 --- a/src/config/ConfigManager.cpp +++ b/src/config/ConfigManager.cpp @@ -250,6 +250,8 @@ void CConfigManager::init() { m_config.addConfigValue("auth:pam:enabled", Hyprlang::INT{1}); m_config.addConfigValue("auth:pam:module", Hyprlang::STRING{"hyprlock"}); + m_config.addConfigValue("auth:pam:retry_on_error", Hyprlang::INT{1}); + m_config.addConfigValue("auth:pam:retry_delay", Hyprlang::INT{3000}); m_config.addConfigValue("auth:fingerprint:enabled", Hyprlang::INT{0}); m_config.addConfigValue("auth:fingerprint:ready_message", Hyprlang::STRING{"(Scan fingerprint to unlock)"}); m_config.addConfigValue("auth:fingerprint:present_message", Hyprlang::STRING{"Scanning fingerprint"});