mcias/data/user.go

144 lines
3.4 KiB
Go

package data
import (
"crypto/subtle"
"errors"
"fmt"
"time"
"github.com/oklog/ulid/v2"
"golang.org/x/crypto/scrypt"
)
const (
scryptN = 32768
scryptR = 8
scryptP = 2
// Constants for derived key length and comparison
derivedKeyLength = 32
validCompareResult = 1
// Empty string constant
emptyString = ""
// TOTP secret length in bytes (160 bits)
totpSecretLength = 20
)
type User struct {
ID string
Created int64
User string
Password []byte
Salt []byte
TOTPSecret string
Roles []string
}
// HasRole checks if the user has a specific role
func (u *User) HasRole(role string) bool {
for _, r := range u.Roles {
if r == role {
return true
}
}
return false
}
// HasPermission checks if the user has a specific permission using the authorization service
func (u *User) HasPermission(authService *AuthorizationService, resource, action string) (bool, error) {
return authService.UserHasPermission(u.ID, resource, action)
}
// GetPermissions returns all permissions for the user using the authorization service
func (u *User) GetPermissions(authService *AuthorizationService) ([]Permission, error) {
return authService.GetUserPermissions(u.ID)
}
type Login struct {
User string `json:"user"`
Password string `json:"password,omitempty"`
Token string `json:"token,omitempty"`
TOTPCode string `json:"totp_code,omitempty"`
}
func derive(password string, salt []byte) ([]byte, error) {
return scrypt.Key([]byte(password), salt, scryptN, scryptR, scryptP, derivedKeyLength)
}
// CheckPassword verifies only the username and password, without TOTP verification
func (u *User) CheckPassword(login *Login) bool {
if u.User != login.User {
return false
}
derived, err := derive(login.Password, u.Salt)
if err != nil {
return false
}
return subtle.ConstantTimeCompare(derived, u.Password) == validCompareResult
}
// Check is a legacy method that now only checks the password
// It's kept for backward compatibility but is equivalent to CheckPassword
func (u *User) Check(login *Login) bool {
// Only check username and password, TOTP verification is now a separate flow
return u.CheckPassword(login)
}
func (u *User) Register(login *Login) error {
var err error
if u.User != emptyString && u.User != login.User {
return errors.New("invalid user")
}
if u.ID == emptyString {
u.ID = ulid.Make().String()
}
u.User = login.User
u.Salt, err = Salt()
if err != nil {
return fmt.Errorf("failed to register user: %w", err)
}
u.Password, err = derive(login.Password, u.Salt)
if err != nil {
return fmt.Errorf("key derivation failed: %w", err)
}
u.Created = time.Now().Unix()
return nil
}
// GenerateTOTPSecret generates a new TOTP secret for the user
func (u *User) GenerateTOTPSecret() (string, error) {
// Generate a random secret
secret, err := GenerateRandomBase32(totpSecretLength)
if err != nil {
return emptyString, fmt.Errorf("failed to generate TOTP secret: %w", err)
}
u.TOTPSecret = secret
return u.TOTPSecret, nil
}
// ValidateTOTPCode validates a TOTP code against the user's TOTP secret
func (u *User) ValidateTOTPCode(code string) (bool, error) {
if u.TOTPSecret == emptyString {
return false, errors.New("TOTP not enabled for user")
}
// Use the twofactor package to validate the code
valid := ValidateTOTP(u.TOTPSecret, code)
return valid, nil
}
// HasTOTP returns true if TOTP is enabled for the user
func (u *User) HasTOTP() bool {
return u.TOTPSecret != emptyString
}