Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions assets/example.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
39 changes: 36 additions & 3 deletions src/auth/Pam.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Hyprlang::STRING>("auth:pam:module");
m_sPamModule = *PAMMODULE;
static const auto PAMMODULE = g_pConfigManager->getValue<Hyprlang::STRING>("auth:pam:module");
static const auto RETRYONERROR = g_pConfigManager->getValue<Hyprlang::INT>("auth:pam:retry_on_error");
static const auto RETRYDELAY = g_pConfigManager->getValue<Hyprlang::INT>("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);
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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;
}
5 changes: 4 additions & 1 deletion src/auth/Pam.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<void()> waitForInput = []() {};
};
Expand All @@ -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 = "";
Expand Down
2 changes: 2 additions & 0 deletions src/config/ConfigManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"});
Expand Down