Fix F-02: replace password-in-hidden-field with nonce
- ui/ui.go: add pendingLogin struct and pendingLogins sync.Map to UIServer; add issueTOTPNonce (generates 128-bit random nonce, stores accountID with 90s TTL) and consumeTOTPNonce (single-use, expiry-checked LoadAndDelete); add dummyHash() method - ui/handlers_auth.go: split handleLoginPost into step 1 (password verify → issue nonce) and step 2 (handleTOTPStep, consume nonce → validate TOTP) via a new finishLogin helper; password never transmitted or stored after step 1 - ui/ui_test.go: refactor newTestMux to reuse new newTestUIServer; add TestTOTPNonceIssuedAndConsumed, TestTOTPNonceUnknownRejected, TestTOTPNonceExpired, and TestLoginPostPasswordNotInTOTPForm; 11/11 tests pass - web/templates/fragments/totp_step.html: replace 'name=password' hidden field with 'name=totp_nonce' - db/accounts.go: add GetAccountByID for TOTP step lookup - AUDIT.md: mark F-02 as fixed Security: the plaintext password previously survived two HTTP round-trips and lived in the browser DOM during the TOTP step. The nonce approach means the password is verified once and immediately discarded; only an opaque random token tied to an account ID (never a credential) crosses the wire on step 2. Nonces are single-use and expire after 90 seconds to limit the window if one is captured.
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
hx-post="/login" hx-target="#login-form" hx-swap="outerHTML">
|
||||
{{if .Error}}<div class="alert alert-error" role="alert">{{.Error}}</div>{{end}}
|
||||
<input type="hidden" name="username" value="{{.Username}}">
|
||||
<input type="hidden" name="password" value="{{.Password}}">
|
||||
<input type="hidden" name="totp_nonce" value="{{.Nonce}}">
|
||||
<input type="hidden" name="totp_step" value="1">
|
||||
<div class="form-group">
|
||||
<label for="totp_code">Authenticator Code</label>
|
||||
|
||||
Reference in New Issue
Block a user