Submission checklist
Feature type
Login UI / visual improvement
Feature summary
On machines where login is normally done with a hardware device — a FIDO2 key, a fingerprint reader, a smartcard — the greeter should not open with a password box. It should show what it is actually waiting for, and reveal the password field only if the device path fails and PAM genuinely asks for a password.
A greeter cannot know the PAM stack in advance, but it does not need to. greetd already tells it, one message at a time. The type of the first auth message after create_session is the signal:
- first message is
secret → classic password stack → show the password box, as today
- first message is
info → device-first stack → a module is waiting on hardware, not on typing
Today the greeter is password-first: it collects input and arms a response before the conversation has said what it wants. On a device-first stack that is backwards, and it produces both a confusing UI and a concrete failure (see Motivation).
Motivation / use case
Two open issues are both really asking for this, and there is a concrete failure that only this design removes.
#35 — "Login requiring both fingerprint and password to sign in"
"when i login I have to first enter my password as well [as] use my fingerprint… on tuigreet im only asked my fingerprint"
That is the password-first UI forcing a typed password onto a device-first stack. allow_empty_password (#37, #45) was the answer, and it works — but as a workaround rather than a model, and it has a sharp edge (below).
#71 — "Login & Unlock with fido2"
FIDO2 login already works today, entirely inside greetd's PAM conversation via pam_u2f — the log below is a working 1FA FIDO2 login with no FIDO2 code in the greeter at all. What is missing is not the capability but the UX. This proposal would give that request a coherent front end without the greeter needing to speak FIDO2 itself.
[INF] [greeter-surface] PAM info message: Please touch the FIDO authenticator.
[DBG] [greeter-surface] greetd reply to post_auth_data: success
[INF] [greeter-surface] authentication successful
The sharp edge on allow_empty_password
With allow_empty_password = true and hardware factors ahead of pam_unix, the password fallback becomes unreachable. tryAuthenticate() arms a pending response from the password box at submit time, and the first secret message consumes it rather than waiting (src/greeter/greeter_surface.cpp):
if (m_hasPendingResponse) {
const std::string response = m_pendingResponse;
m_hasPendingResponse = false;
m_pendingResponse.clear();
postAuthResponse(response);
return;
}
m_secretPromptWaiting = true; // only reached on LATER secret prompts
That is correct when the first secret is the password prompt. It breaks when info messages precede it: the pending response is still armed and, after an empty submit, is an empty string. The real Password: prompt consumes it, pam_unix gets 0 bytes, and the session restarts in a loop:
[INF] [greeter-surface] PAM info message: Place finger on scanner (left-index, right-index)...
[DBG] [greeter-surface] greetd reply to post_auth_data: auth_message
[INF] [greeter-surface] PAM secret message: Password:
[DBG] [greeter-surface] greetd: post_auth_data (0 bytes) <-- never waited for input
[DBG] [greeter-surface] greetd reply to post_auth_data: error
[WRN] [greeter-surface] authentication failed: pam_authenticate: AUTHTOK_ERR
pam_unix(greetd:auth): conversation failed
pam_unix(greetd:auth): auth could not identify password for [<user>]
To reproduce: any auth module emitting PAM_TEXT_INFO before pam_unix (pam_u2f … cue alone is enough), plus allow_empty_password = true. Press Login with the field empty and let the devices time out. Expected: the Password: prompt appears and waits. Actual: an empty response is posted immediately, auth fails with AUTHTOK_ERR, and it loops. Typing while an info cue is displayed has no effect — the response was committed at submit time.
Under the proposed design this cannot happen, because nothing is armed before the conversation asks for it.
The UI state users actually see
While the cascade waits on a device, the password box stays enabled and accepts keystrokes. The status line reads "Place finger on scanner (left-index, right-index)…" while the field fills with dots that can never be submitted anywhere.
That is what leads people to conclude the greeter "requires a password before the fingerprint prompt", or that the authentication order is wrong. Neither is true — pam_u2f runs first and short-circuits correctly — but the UI gives no way to tell.
Proposed solution
Let the conversation drive the UI
- On submit, send
create_session and enter a neutral waiting state rather than arming a response.
- While
info messages arrive, show a device affordance — an icon plus the module's own prompt text ("Please touch the FIDO authenticator.", "Place finger on scanner (…)"). No text input, because none can be answered at this point. Auto-post the empty response, as today.
- Reveal the password field only when a
secret message actually arrives, then wait for real input.
- If a device fails and the cascade moves on, the next
info replaces the affordance; if it falls through to pam_unix, step 3 shows the password box at exactly the right moment.
This makes the password box appear only when a password is genuinely being asked for, which is what a user on a key-or-fingerprint machine expects. It degrades correctly for a machine with one device, and for a machine with none.
A per-module icon is not required. A single generic "waiting on authentication device" affordance carrying the PAM-supplied text would already be most of the value, since that text comes from the module and is therefore always accurate.
allow_empty_password would become unnecessary under this model: an empty submit stops being a way of saying "let the hardware handle it", because the greeter would not be asking for a password in the first place.
Interim fix, if the redesign is a larger piece of work
Only consume the pending response when it is meaningful — either skip that branch when m_pendingResponse.empty() && m_allowEmptyPassword, or clear m_hasPendingResponse once an Info message has been auto-answered. That restores the password fallback without changing the UI model.
For reference, ReGreet (0.5.0, src/gui/model.rs) does not hit this: it sets input_prompt and waits on a Secret message, auto-posting an empty response only for Info and Error.
Environment
|
|
| noctalia-greeter |
1.1.0-1 (distro package; binary reports 1.0.0, cf. #88) |
| greetd |
0.10.3 |
| Distro |
CachyOS (Arch-based), kernel 7.1.5 |
| Session |
Hyprland (uwsm-managed) |
Login hardware, both used daily as the primary way into this machine:
- YubiKey 5 (OTP+U2F+CCID) via
pam-u2f 1.4.0 with cue, so the module emits "Please touch the FIDO authenticator." as PAM_TEXT_INFO.
- SecuGen U20 fingerprint reader via
pam_sgfp.so. The vendor ships no Linux PAM integration for this device, so we wrote the module in-house against their SDK. It emits "Place finger on scanner (…)" as PAM_TEXT_INFO and supports N capture retries.
/etc/pam.d/greetd includes system-local-login:
auth sufficient pam_u2f.so authfile=/etc/u2f_mappings cue
auth sufficient pam_sgfp.so retries=4
auth include system-login # -> pam_unix
All three are sufficient — alternatives, not additional factors. greetd runs this conversation itself as root and relays it over its IPC socket, so the cascade is greeter-independent. Verified end to end with pamtester -v greetd <user> authenticate for all three factors, both before and after installing noctalia-greeter.
Current workaround
Keep allow_empty_password = true and type a password before submitting whenever the fallback might be needed — a non-empty pending response is consumed correctly, while the devices still short-circuit first if used. Setting allow_empty_password = false does not help: a typed password behaves identically either way, so it only removes the empty-submit fast path.
Alternatives considered
Setting allow_empty_password = false. Tried, and it is strictly worse. A typed password behaves identically under both settings — the pending response is non-empty either way, so the secret prompt consumes it correctly. false only removes the empty-submit fast path, and gains nothing, because the underlying UI model is unchanged.
Reordering the PAM stack to put pam_unix first. Would make the password prompt appear immediately, but inverts the intent — the hardware devices are the primary login method here, not the fallback — and it would make pam_faillock count a failed attempt on every device-based login.
Fixing only the pending-response consumption (the interim fix above). Restores the password fallback, but leaves the confusing UI state: a password box that accepts keystrokes while the status line says the greeter is waiting on a fingerprint. It treats the symptom.
Staying on ReGreet. ReGreet handles the conversation correctly and shows a configurable greeting string that can explain the device flow, but it does not match the Noctalia shell visually, which is the reason for switching in the first place.
References / related projects
Prior art for the message-driven approach:
- ReGreet — https://github.com/rharish101/ReGreet —
src/gui/model.rs sets input_prompt and waits on a Secret message, auto-posting an empty response only for Info and Error.
- greetd IPC protocol — https://man.sr.ht/~kennylevinsen/greetd/ —
auth_message_type is one of visible, secret, info, error, which is the signal this proposal relies on.
pam_u2f(8) cue / cue_prompt — the module supplies its own user-facing text, so a greeter never has to invent device-specific wording.
Additional context
No response
Submission checklist
Feature type
Login UI / visual improvement
Feature summary
On machines where login is normally done with a hardware device — a FIDO2 key, a fingerprint reader, a smartcard — the greeter should not open with a password box. It should show what it is actually waiting for, and reveal the password field only if the device path fails and PAM genuinely asks for a password.
A greeter cannot know the PAM stack in advance, but it does not need to. greetd already tells it, one message at a time. The type of the first auth message after
create_sessionis the signal:secret→ classic password stack → show the password box, as todayinfo→ device-first stack → a module is waiting on hardware, not on typingToday the greeter is password-first: it collects input and arms a response before the conversation has said what it wants. On a device-first stack that is backwards, and it produces both a confusing UI and a concrete failure (see Motivation).
Motivation / use case
Two open issues are both really asking for this, and there is a concrete failure that only this design removes.
#35 — "Login requiring both fingerprint and password to sign in"
That is the password-first UI forcing a typed password onto a device-first stack.
allow_empty_password(#37, #45) was the answer, and it works — but as a workaround rather than a model, and it has a sharp edge (below).#71 — "Login & Unlock with fido2"
FIDO2 login already works today, entirely inside greetd's PAM conversation via
pam_u2f— the log below is a working 1FA FIDO2 login with no FIDO2 code in the greeter at all. What is missing is not the capability but the UX. This proposal would give that request a coherent front end without the greeter needing to speak FIDO2 itself.The sharp edge on
allow_empty_passwordWith
allow_empty_password = trueand hardware factors ahead ofpam_unix, the password fallback becomes unreachable.tryAuthenticate()arms a pending response from the password box at submit time, and the firstsecretmessage consumes it rather than waiting (src/greeter/greeter_surface.cpp):That is correct when the first
secretis the password prompt. It breaks wheninfomessages precede it: the pending response is still armed and, after an empty submit, is an empty string. The realPassword:prompt consumes it,pam_unixgets 0 bytes, and the session restarts in a loop:To reproduce: any
authmodule emittingPAM_TEXT_INFObeforepam_unix(pam_u2f … cuealone is enough), plusallow_empty_password = true. Press Login with the field empty and let the devices time out. Expected: thePassword:prompt appears and waits. Actual: an empty response is posted immediately, auth fails withAUTHTOK_ERR, and it loops. Typing while an info cue is displayed has no effect — the response was committed at submit time.Under the proposed design this cannot happen, because nothing is armed before the conversation asks for it.
The UI state users actually see
While the cascade waits on a device, the password box stays enabled and accepts keystrokes. The status line reads "Place finger on scanner (left-index, right-index)…" while the field fills with dots that can never be submitted anywhere.
That is what leads people to conclude the greeter "requires a password before the fingerprint prompt", or that the authentication order is wrong. Neither is true —
pam_u2fruns first and short-circuits correctly — but the UI gives no way to tell.Proposed solution
Let the conversation drive the UI
create_sessionand enter a neutral waiting state rather than arming a response.infomessages arrive, show a device affordance — an icon plus the module's own prompt text ("Please touch the FIDO authenticator.", "Place finger on scanner (…)"). No text input, because none can be answered at this point. Auto-post the empty response, as today.secretmessage actually arrives, then wait for real input.inforeplaces the affordance; if it falls through topam_unix, step 3 shows the password box at exactly the right moment.This makes the password box appear only when a password is genuinely being asked for, which is what a user on a key-or-fingerprint machine expects. It degrades correctly for a machine with one device, and for a machine with none.
A per-module icon is not required. A single generic "waiting on authentication device" affordance carrying the PAM-supplied text would already be most of the value, since that text comes from the module and is therefore always accurate.
allow_empty_passwordwould become unnecessary under this model: an empty submit stops being a way of saying "let the hardware handle it", because the greeter would not be asking for a password in the first place.Interim fix, if the redesign is a larger piece of work
Only consume the pending response when it is meaningful — either skip that branch when
m_pendingResponse.empty() && m_allowEmptyPassword, or clearm_hasPendingResponseonce anInfomessage has been auto-answered. That restores the password fallback without changing the UI model.For reference, ReGreet (0.5.0,
src/gui/model.rs) does not hit this: it setsinput_promptand waits on aSecretmessage, auto-posting an empty response only forInfoandError.Environment
1.0.0, cf. #88)Login hardware, both used daily as the primary way into this machine:
pam-u2f1.4.0 withcue, so the module emits "Please touch the FIDO authenticator." asPAM_TEXT_INFO.pam_sgfp.so. The vendor ships no Linux PAM integration for this device, so we wrote the module in-house against their SDK. It emits "Place finger on scanner (…)" asPAM_TEXT_INFOand supports N capture retries./etc/pam.d/greetdincludessystem-local-login:All three are
sufficient— alternatives, not additional factors. greetd runs this conversation itself as root and relays it over its IPC socket, so the cascade is greeter-independent. Verified end to end withpamtester -v greetd <user> authenticatefor all three factors, both before and after installing noctalia-greeter.Current workaround
Keep
allow_empty_password = trueand type a password before submitting whenever the fallback might be needed — a non-empty pending response is consumed correctly, while the devices still short-circuit first if used. Settingallow_empty_password = falsedoes not help: a typed password behaves identically either way, so it only removes the empty-submit fast path.Alternatives considered
Setting
allow_empty_password = false. Tried, and it is strictly worse. A typed password behaves identically under both settings — the pending response is non-empty either way, so thesecretprompt consumes it correctly.falseonly removes the empty-submit fast path, and gains nothing, because the underlying UI model is unchanged.Reordering the PAM stack to put
pam_unixfirst. Would make the password prompt appear immediately, but inverts the intent — the hardware devices are the primary login method here, not the fallback — and it would makepam_faillockcount a failed attempt on every device-based login.Fixing only the pending-response consumption (the interim fix above). Restores the password fallback, but leaves the confusing UI state: a password box that accepts keystrokes while the status line says the greeter is waiting on a fingerprint. It treats the symptom.
Staying on ReGreet. ReGreet handles the conversation correctly and shows a configurable greeting string that can explain the device flow, but it does not match the Noctalia shell visually, which is the reason for switching in the first place.
References / related projects
allow_empty_passwordwork this builds onPrior art for the message-driven approach:
src/gui/model.rssetsinput_promptand waits on aSecretmessage, auto-posting an empty response only forInfoandError.auth_message_typeis one ofvisible,secret,info,error, which is the signal this proposal relies on.pam_u2f(8)cue/cue_prompt— the module supplies its own user-facing text, so a greeter never has to invent device-specific wording.Additional context
No response