Files
mcias/internal/validate/validate.go
Kyle Isom 0ad9ef1bb4 Fix F-08, F-12, F-13: Implement account lockout, username validation, and password minimum length enforcement
- Added failed login tracking for account lockout enforcement in `db` and `ui` layers; introduced `failed_logins` table to store attempts, window start, and attempt count.
- Updated login checks in `grpcserver/auth.go` and `ui/handlers_auth.go` to reject requests if the account is locked.
- Added immediate failure counter reset on successful login.
- Implemented username length and character set validation (F-12) and minimum password length enforcement (F-13) in shared `validate` package.
- Updated account creation and edit flows in `ui` and `grpcserver` layers to apply validation before hashing/processing.
- Added comprehensive unit tests for lockout, validation, and related edge cases.
- Updated `AUDIT.md` to mark F-08, F-12, and F-13 as fixed.
- Updated `openapi.yaml` to reflect new validation and lockout behaviors.

Security: Prevents brute-force attacks via lockout mechanism and strengthens defenses against weak and invalid input.
2026-03-11 20:59:26 -07:00

56 lines
2.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package validate provides shared input-validation helpers used by the REST,
// gRPC, and UI handlers.
package validate
import (
"fmt"
"regexp"
)
// usernameRE is the allowed character set for usernames: alphanumeric plus a
// small set of punctuation that is safe in all contexts (URLs, HTML, logs).
// Length is enforced separately so the error message can be more precise.
//
// Security (F-12): rejecting control characters, null bytes, newlines, and
// unusual Unicode prevents log injection, stored-XSS via username display,
// and rendering anomalies in the admin UI.
var usernameRE = regexp.MustCompile(`^[a-zA-Z0-9._@-]+$`)
// MinUsernameLen and MaxUsernameLen are the inclusive bounds on username length.
const (
MinUsernameLen = 1
MaxUsernameLen = 255
)
// Username returns nil if the username is valid, or a descriptive error if not.
// Valid usernames are 1255 characters long and contain only alphanumeric
// characters and the symbols . _ @ -
func Username(username string) error {
l := len(username)
if l < MinUsernameLen || l > MaxUsernameLen {
return fmt.Errorf("username must be between %d and %d characters", MinUsernameLen, MaxUsernameLen)
}
if !usernameRE.MatchString(username) {
return fmt.Errorf("username may only contain letters, digits, and the characters . _ @ -")
}
return nil
}
// MinPasswordLen is the minimum acceptable plaintext password length.
//
// Security (F-13): NIST SP 800-63B recommends a minimum of 8 characters;
// we use 12 to provide additional margin against offline brute-force attacks
// even though Argon2id is expensive. The check is performed at the handler
// level (before hashing) so Argon2id is never invoked with a trivially weak
// password.
const MinPasswordLen = 12
// Password returns nil if the plaintext password meets the minimum length
// requirement, or a descriptive error if not.
func Password(password string) error {
if len(password) < MinPasswordLen {
return fmt.Errorf("password must be at least %d characters", MinPasswordLen)
}
return nil
}