Add HTMX-based UI templates and handlers for account and audit management
- Introduced `web/templates/` for HTMX-fragmented pages (`dashboard`, `accounts`, `account_detail`, `error_fragment`, etc.). - Implemented UI routes for account CRUD, audit log display, and login/logout with CSRF protection. - Added `internal/ui/` package for handlers, CSRF manager, session validation, and token issuance. - Updated documentation to include new UI features and templates directory structure. - Security: Double-submit CSRF cookies, constant-time HMAC validation, login password/Argon2id re-verification at all steps to prevent bypass.
This commit is contained in:
65
internal/ui/csrf.go
Normal file
65
internal/ui/csrf.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// Package ui provides the HTMX-based management web interface for MCIAS.
|
||||
package ui
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// CSRFManager implements HMAC-signed Double-Submit Cookie CSRF protection.
|
||||
//
|
||||
// Security design:
|
||||
// - The CSRF key is derived from the server master key via SHA-256 with a
|
||||
// domain-separation prefix, so it is unique to the UI CSRF function.
|
||||
// - The cookie value is 32 bytes of cryptographic random (non-HttpOnly so
|
||||
// HTMX can read it via JavaScript-free double-submit; SameSite=Strict
|
||||
// provides the primary CSRF defence for browser-initiated requests).
|
||||
// - The form/header value is HMAC-SHA256(key, cookieVal); this is what the
|
||||
// server verifies. An attacker cannot forge the HMAC without the key.
|
||||
// - Comparison uses crypto/subtle.ConstantTimeCompare to prevent timing attacks.
|
||||
type CSRFManager struct {
|
||||
key []byte
|
||||
}
|
||||
|
||||
// newCSRFManager creates a CSRFManager whose key is derived from masterKey.
|
||||
// Key derivation: SHA-256("mcias-ui-csrf-v1" || masterKey)
|
||||
func newCSRFManager(masterKey []byte) *CSRFManager {
|
||||
h := sha256.New()
|
||||
h.Write([]byte("mcias-ui-csrf-v1"))
|
||||
h.Write(masterKey)
|
||||
return &CSRFManager{key: h.Sum(nil)}
|
||||
}
|
||||
|
||||
// NewToken generates a fresh CSRF token pair.
|
||||
//
|
||||
// Returns:
|
||||
// - cookieVal: hex(32 random bytes) — stored in the mcias_csrf cookie
|
||||
// - headerVal: hex(HMAC-SHA256(key, cookieVal)) — embedded in forms / X-CSRF-Token header
|
||||
func (c *CSRFManager) NewToken() (cookieVal, headerVal string, err error) {
|
||||
raw := make([]byte, 32)
|
||||
if _, err = rand.Read(raw); err != nil {
|
||||
return "", "", fmt.Errorf("csrf: generate random bytes: %w", err)
|
||||
}
|
||||
cookieVal = hex.EncodeToString(raw)
|
||||
mac := hmac.New(sha256.New, c.key)
|
||||
mac.Write([]byte(cookieVal))
|
||||
headerVal = hex.EncodeToString(mac.Sum(nil))
|
||||
return cookieVal, headerVal, nil
|
||||
}
|
||||
|
||||
// Validate verifies that headerVal is the correct HMAC of cookieVal.
|
||||
// Returns false on any mismatch or decoding error.
|
||||
func (c *CSRFManager) Validate(cookieVal, headerVal string) bool {
|
||||
if cookieVal == "" || headerVal == "" {
|
||||
return false
|
||||
}
|
||||
mac := hmac.New(sha256.New, c.key)
|
||||
mac.Write([]byte(cookieVal))
|
||||
expected := hex.EncodeToString(mac.Sum(nil))
|
||||
// Security: constant-time comparison prevents timing oracle attacks.
|
||||
return subtle.ConstantTimeCompare([]byte(expected), []byte(headerVal)) == 1
|
||||
}
|
||||
Reference in New Issue
Block a user