Skip to content

[FEATURE] Device-first authentication UI — let the PAM conversation drive the prompt #90

Description

@antojk

Submission checklist

  • I have searched existing issues and confirmed this has not been requested before.
  • I have checked existing pull requests for similar changes.

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

  1. On submit, send create_session and enter a neutral waiting state rather than arming a response.
  2. 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.
  3. Reveal the password field only when a secret message actually arrives, then wait for real input.
  4. 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/ReGreetsrc/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

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureNew feature or requestuiFeature request category

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions