Add TOTP enrollment to web UI
- Profile page TOTP section with enrollment flow: password re-auth → QR code + manual entry → 6-digit confirm - Server-side QR code generation (go-qrcode, data: URI PNG) - Admin "Remove TOTP" button on account detail page - Enrollment nonces: sync.Map with 5-minute TTL, single-use - Template fragments: totp_section.html, totp_enroll_qr.html - Handler: handlers_totp.go (enroll start, confirm, admin remove) Security: Password re-auth before secret generation (SEC-01). Lockout checked before Argon2. CSRF on all endpoints. Single-use enrollment nonces with expiry. TOTP counter replay prevention (CRIT-01). Self-removal not permitted (admin only). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,18 @@
|
||||
<dt class="text-muted">Type</dt><dd>{{.Account.AccountType}}</dd>
|
||||
<dt class="text-muted">Status</dt>
|
||||
<dd id="status-cell">{{template "account_status" .}}</dd>
|
||||
<dt class="text-muted">TOTP</dt><dd>{{if .Account.TOTPRequired}}Enabled{{else}}Disabled{{end}}</dd>
|
||||
<dt class="text-muted">TOTP</dt>
|
||||
<dd id="totp-admin-status">
|
||||
{{if .Account.TOTPRequired}}
|
||||
Enabled
|
||||
<button class="btn btn-sm btn-danger" style="margin-left:.5rem"
|
||||
hx-delete="/accounts/{{.Account.UUID}}/totp"
|
||||
hx-target="#totp-admin-status"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Remove TOTP for this account?"
|
||||
hx-headers='{"X-CSRF-Token": "{{.CSRFToken}}"}'>Remove</button>
|
||||
{{else}}Disabled{{end}}
|
||||
</dd>
|
||||
{{if .WebAuthnEnabled}}<dt class="text-muted">Passkeys</dt><dd>{{len .WebAuthnCreds}} registered</dd>{{end}}
|
||||
<dt class="text-muted">Created</dt><dd class="text-small">{{formatTime .Account.CreatedAt}}</dd>
|
||||
<dt class="text-muted">Updated</dt><dd class="text-small">{{formatTime .Account.UpdatedAt}}</dd>
|
||||
|
||||
35
web/templates/fragments/totp_enroll_qr.html
Normal file
35
web/templates/fragments/totp_enroll_qr.html
Normal file
@@ -0,0 +1,35 @@
|
||||
{{define "totp_enroll_qr"}}
|
||||
<div id="totp-section">
|
||||
{{if .TOTPError}}<div class="alert alert-error" role="alert">{{.TOTPError}}</div>{{end}}
|
||||
<p class="text-small" style="margin-bottom:.75rem">
|
||||
Scan this QR code with your authenticator app, then enter the 6-digit code to confirm.
|
||||
</p>
|
||||
<div style="text-align:center;margin:1rem 0">
|
||||
<img src="{{.TOTPQR}}" alt="TOTP QR Code" width="200" height="200"
|
||||
style="image-rendering:pixelated">
|
||||
</div>
|
||||
<details style="margin-bottom:1rem">
|
||||
<summary class="text-small text-muted" style="cursor:pointer">Manual entry</summary>
|
||||
<code style="font-size:.8rem;word-break:break-all;display:block;margin-top:.5rem;
|
||||
padding:.5rem;background:var(--color-bg-alt,#f5f5f5);border-radius:4px">
|
||||
{{.TOTPSecret}}
|
||||
</code>
|
||||
</details>
|
||||
<form hx-post="/profile/totp/confirm"
|
||||
hx-target="#totp-section"
|
||||
hx-swap="outerHTML"
|
||||
hx-headers='{"X-CSRF-Token": "{{.CSRFToken}}"}'>
|
||||
<input type="hidden" name="totp_enroll_nonce" value="{{.TOTPEnrollNonce}}">
|
||||
<div class="form-group">
|
||||
<label for="totp-confirm-code">Authenticator Code</label>
|
||||
<input type="text" id="totp-confirm-code" name="totp_code"
|
||||
class="form-control" autocomplete="one-time-code"
|
||||
inputmode="numeric" pattern="[0-9]{6}" maxlength="6"
|
||||
required autofocus placeholder="6-digit code">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm" style="margin-top:.5rem">
|
||||
Verify & Enable
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
29
web/templates/fragments/totp_section.html
Normal file
29
web/templates/fragments/totp_section.html
Normal file
@@ -0,0 +1,29 @@
|
||||
{{define "totp_section"}}
|
||||
<div id="totp-section">
|
||||
{{if .TOTPSuccess}}<div class="alert alert-success" role="alert">{{.TOTPSuccess}}</div>{{end}}
|
||||
{{if .TOTPEnabled}}
|
||||
<p class="text-small" style="margin-bottom:.5rem">
|
||||
<span style="color:var(--color-success,#27ae60);font-weight:600">✓ Enabled</span>
|
||||
</p>
|
||||
<p class="text-muted text-small">To remove TOTP, contact an administrator.</p>
|
||||
{{else}}
|
||||
<p class="text-muted text-small" style="margin-bottom:.75rem">
|
||||
Add a time-based one-time password for two-factor authentication.
|
||||
</p>
|
||||
{{if .TOTPError}}<div class="alert alert-error" role="alert">{{.TOTPError}}</div>{{end}}
|
||||
<form hx-post="/profile/totp/enroll"
|
||||
hx-target="#totp-section"
|
||||
hx-swap="outerHTML"
|
||||
hx-headers='{"X-CSRF-Token": "{{.CSRFToken}}"}'>
|
||||
<div class="form-group">
|
||||
<label for="totp-enroll-password">Current Password</label>
|
||||
<input type="password" id="totp-enroll-password" name="password"
|
||||
class="form-control" autocomplete="current-password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm" style="margin-top:.5rem">
|
||||
Set Up Authenticator
|
||||
</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -4,6 +4,10 @@
|
||||
<div class="page-header">
|
||||
<h1>Profile</h1>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 style="font-size:1rem;font-weight:600;margin-bottom:1rem">Two-Factor Authentication (TOTP)</h2>
|
||||
{{template "totp_section" .}}
|
||||
</div>
|
||||
{{if .WebAuthnEnabled}}
|
||||
<div class="card">
|
||||
<h2 style="font-size:1rem;font-weight:600;margin-bottom:1rem">Passkeys</h2>
|
||||
|
||||
Reference in New Issue
Block a user