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.
This commit is contained in:
2026-03-11 20:59:26 -07:00
parent 6e690c4435
commit 0ad9ef1bb4
13 changed files with 1487 additions and 15 deletions

View File

@@ -15,6 +15,7 @@ import (
"git.wntrmute.dev/kyle/mcias/internal/auth"
"git.wntrmute.dev/kyle/mcias/internal/db"
"git.wntrmute.dev/kyle/mcias/internal/model"
"git.wntrmute.dev/kyle/mcias/internal/validate"
)
type accountServiceServer struct {
@@ -58,8 +59,9 @@ func (a *accountServiceServer) CreateAccount(ctx context.Context, req *mciasv1.C
if err := a.s.requireAdmin(ctx); err != nil {
return nil, err
}
if req.Username == "" {
return nil, status.Error(codes.InvalidArgument, "username is required")
// Security (F-12): validate username length and character set.
if err := validate.Username(req.Username); err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
accountType := model.AccountType(req.AccountType)
if accountType != model.AccountTypeHuman && accountType != model.AccountTypeSystem {
@@ -71,6 +73,10 @@ func (a *accountServiceServer) CreateAccount(ctx context.Context, req *mciasv1.C
if req.Password == "" {
return nil, status.Error(codes.InvalidArgument, "password is required for human accounts")
}
// Security (F-13): enforce minimum length before hashing.
if err := validate.Password(req.Password); err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
var err error
passwordHash, err = auth.HashPassword(req.Password, auth.ArgonParams{
Time: a.s.cfg.Argon2.Time,