Files
volumen/internal/admin/auth.go
T
petrbalvin f8ed33df83
Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s
Initial commit
Assisted-by: GLM 5.3
2026-09-29 10:03:32 +02:00

110 lines
3.5 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
package admin
import (
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"net/http"
"strings"
"unicode"
"golang.org/x/text/unicode/norm"
"sourcedock.dev/petrbalvin/volumen/internal/session"
)
// commonPasswords is the blocklist of trivially guessable passwords.
// This is the policy every admin password change goes through.
var commonPasswords = map[string]bool{
"password": true, "password1": true, "password123": true,
"123456": true, "12345678": true, "123456789": true,
"qwerty": true, "qwerty123": true, "letmein": true, "iloveyou": true,
"admin": true, "admin123": true, "welcome": true, "welcome1": true,
"monkey": true, "dragon": true, "football": true, "baseball": true,
"sunshine": true, "princess": true, "abc123": true, "111111": true,
"123123": true, "1q2w3e4r": true, "passw0rd": true, "trustno1": true,
"changeme": true, "secret": true, "secret123": true, "test": true,
"test123": true, "guest": true, "master": true, "000000": true,
"696969": true, "qwertyuiop": true, "superman": true, "batman": true,
"jordan": true, "harley": true, "hunter": true, "hunter2": true,
"shadow": true, "michael": true, "jennifer": true, "abcdef": true,
"abcdefg": true,
}
// PasswordError validates a newly chosen password and reports the
// first problem as a catalogue key. The key is either a plain sentence or
// the id of a plural message whose numeral n is the offending length;
// "" with n 0 means accepted.
func PasswordError(password string, minLength, maxLength int) (string, int) {
if strings.TrimSpace(password) == "" {
return "New password cannot be empty.", 0
}
length := len([]rune(password))
if length < minLength {
return "password.min", minLength
}
if length > maxLength {
return "password.max", maxLength
}
normalized := strings.ToLower(norm.NFKC.String(password))
if commonPasswords[normalized] {
return "This password is too common.", 0
}
return "", 0
}
// CSRFToken returns (and lazily creates) the CSRF token stored in the
// session.
func CSRFToken(sess *session.Session) string {
token := sess.Get("csrf")
if token == "" {
token = newTokenHex(32)
sess.Set("csrf", token)
}
return token
}
// sessionFingerprint derives the value the session carries to bind it to
// one password: it changes whenever the account's hash changes, so a
// password change or an admin reset retires every cookie issued before
// it. It is a digest of the stored hash, never of the password, and
// carries too few bits to help anyone invert the hash.
func sessionFingerprint(storedHash string) string {
sum := sha256.Sum256([]byte("volumen-session-v1:" + storedHash))
return hex.EncodeToString(sum[:8])
}
// ValidateCSRF compares the form's _csrf field against the session
// token in constant time.
func ValidateCSRF(r *http.Request, sess *session.Session) bool {
token := r.PostFormValue("_csrf")
sessionToken := sess.Get("csrf")
if token == "" || sessionToken == "" {
return false
}
return subtle.ConstantTimeCompare([]byte(sessionToken), []byte(token)) == 1
}
func newTokenHex(nBytes int) string {
buf := make([]byte, nBytes)
if _, err := rand.Read(buf); err != nil {
return ""
}
return hex.EncodeToString(buf)
}
// firstUpper returns the uppercased first rune, or fallback.
func firstUpper(s, fallback string) string {
for _, r := range s {
if unicode.IsSpace(r) {
continue
}
return string(unicode.ToUpper(r))
}
return fallback
}