From a4b7bd1bed42bc9e41532fa2c89f28160fb173e3 Mon Sep 17 00:00:00 2001 From: Abhishek Shroff Date: Tue, 10 Jun 2025 02:38:03 +0530 Subject: [PATCH] [server][auth] Create auth package with extensible config for LDAP and OAuth --- .../api/authenticator/authenticator.go | 12 +- server/internal/api/v1/auth/routes.go | 13 +- server/internal/api/webdav/handler.go | 5 +- server/internal/auth/auth.go | 151 ++++++++++++++++++ server/internal/auth/conifg.go | 21 +++ server/internal/{ => auth}/crypt/argon2.go | 0 server/internal/{ => auth}/crypt/config.go | 0 server/internal/{ => auth}/crypt/crypt.go | 0 server/internal/auth/strength.go | 64 ++++++++ server/internal/command/admin/user/passwd.go | 4 +- server/internal/command/admin/user/pwreset.go | 4 +- server/internal/command/command.go | 7 +- server/internal/command/config.defaults.yml | 30 ++-- server/internal/command/config.go | 4 +- server/internal/core/config.go | 9 -- server/internal/core/core.go | 8 - server/internal/core/filesystem_readonly.go | 2 +- server/internal/core/password.go | 57 ------- server/internal/core/resource_publink.go | 2 +- server/internal/core/user_auth.go | 118 -------------- server/internal/core/user_manager.go | 16 -- server/internal/core/user_update.go | 17 -- 22 files changed, 284 insertions(+), 260 deletions(-) create mode 100644 server/internal/auth/auth.go create mode 100644 server/internal/auth/conifg.go rename server/internal/{ => auth}/crypt/argon2.go (100%) rename server/internal/{ => auth}/crypt/config.go (100%) rename server/internal/{ => auth}/crypt/crypt.go (100%) create mode 100644 server/internal/auth/strength.go delete mode 100644 server/internal/core/password.go delete mode 100644 server/internal/core/user_auth.go diff --git a/server/internal/api/authenticator/authenticator.go b/server/internal/api/authenticator/authenticator.go index c7eb7fc1..cbdb43e4 100644 --- a/server/internal/api/authenticator/authenticator.go +++ b/server/internal/api/authenticator/authenticator.go @@ -5,6 +5,7 @@ import ( "net/http" "strings" + "codeberg.org/shroff/phylum/server/internal/auth" "codeberg.org/shroff/phylum/server/internal/core" "github.com/gin-gonic/gin" ) @@ -41,11 +42,10 @@ func Require(c *gin.Context) { } func extractUserDetails(c *gin.Context) (core.User, error) { - userManager := core.UserManagerFromContext(c.Request.Context()) if header := c.Request.Header.Get("Authorization"); header == "" { if cookie, err := c.Request.Cookie("auth_token"); err == nil { token := cookie.Value - if u, err := userManager.ReadAccessToken(token); err == nil { + if u, err := auth.ReadAccessToken(c.Request.Context(), token); err == nil { return u, nil } else { return core.User{}, err @@ -54,16 +54,16 @@ func extractUserDetails(c *gin.Context) (core.User, error) { return core.User{}, err } return core.User{}, errAuthRequired - } else if auth, ok := checkAuthHeader(header, "basic"); ok { - if email, password, ok := decodeBasicAuth(auth); ok { - if u, err := userManager.VerifyUserPassword(email, password); err == nil { + } else if authHeader, ok := checkAuthHeader(header, "basic"); ok { + if email, password, ok := decodeBasicAuth(authHeader); ok { + if u, err := auth.VerifyUserPassword(c.Request.Context(), email, password); err == nil { return u, nil } else { return core.User{}, err } } } else if token, ok := checkAuthHeader(header, "bearer"); ok { - if u, err := userManager.ReadAccessToken(token); err == nil { + if u, err := auth.ReadAccessToken(c.Request.Context(), token); err == nil { return u, nil } else { return core.User{}, err diff --git a/server/internal/api/v1/auth/routes.go b/server/internal/api/v1/auth/routes.go index a3710b1d..7be14d77 100644 --- a/server/internal/api/v1/auth/routes.go +++ b/server/internal/api/v1/auth/routes.go @@ -7,7 +7,9 @@ import ( "codeberg.org/shroff/phylum/server/internal/api/authenticator" "codeberg.org/shroff/phylum/server/internal/api/v1/my" "codeberg.org/shroff/phylum/server/internal/api/v1/responses" + "codeberg.org/shroff/phylum/server/internal/auth" "codeberg.org/shroff/phylum/server/internal/core" + "codeberg.org/shroff/phylum/server/internal/db" "codeberg.org/shroff/phylum/server/internal/mail" "github.com/gin-gonic/gin" ) @@ -42,10 +44,9 @@ func handlePasswordAuth(c *gin.Context) { panic(core.NewError(http.StatusBadRequest, "missing_params", "Email or password not specified")) } - userManager := core.UserManagerFromContext(c.Request.Context()) - if user, err := userManager.VerifyUserPassword(params.Email, params.Password); err != nil { + if user, err := auth.VerifyUserPassword(c.Request.Context(), params.Email, params.Password); err != nil { panic(err) - } else if token, err := userManager.CreateAccessToken(user); err != nil { + } else if token, err := auth.CreateAccessToken(c.Request.Context(), user); err != nil { panic(err) } else if bootstrap, err := my.Bootstrap(c.Request.Context(), user, 0); err != nil { panic(err) @@ -75,7 +76,7 @@ func handleRequestPasswordReset(c *gin.Context) { panic(err) } - if token, err := userManager.CreateResetToken(u); err != nil { + if token, err := auth.CreateResetToken(db.Get(c.Request.Context()), u); err != nil { panic(err) } else { go func() { @@ -95,9 +96,9 @@ func handleResetPassword(c *gin.Context) { userManager := core.UserManagerFromContext(c.Request.Context()) if user, err := userManager.UserByEmail(params.Email); err != nil { panic(err) - } else if err := userManager.ResetUserPassword(user, params.Token, params.Password); err != nil { + } else if err := auth.ResetUserPassword(db.Get(c.Request.Context()), user, params.Token, params.Password); err != nil { panic(err) - } else if token, err := userManager.CreateAccessToken(user); err != nil { + } else if token, err := auth.CreateAccessToken(c.Request.Context(), user); err != nil { panic(err) } else if bootstrap, err := my.Bootstrap(c.Request.Context(), user, 0); err != nil { panic(err) diff --git a/server/internal/api/webdav/handler.go b/server/internal/api/webdav/handler.go index ea0e1f17..ede55892 100644 --- a/server/internal/api/webdav/handler.go +++ b/server/internal/api/webdav/handler.go @@ -5,6 +5,7 @@ import ( "net/http" webdav "codeberg.org/shroff/phylum/server/internal/api/webdav/impl" + "codeberg.org/shroff/phylum/server/internal/auth" "codeberg.org/shroff/phylum/server/internal/core" "github.com/gin-gonic/gin" "github.com/google/uuid" @@ -44,7 +45,7 @@ func (h *handler) HandleRequest(c *gin.Context) { if email, pass, ok := c.Request.BasicAuth(); ok { ctx := c.Request.Context() userManager := core.UserManagerFromContext(ctx) - if u, err := userManager.VerifyUserPassword(email, pass); err == nil { + if u, err := auth.VerifyUserPassword(ctx, email, pass); err == nil { authSuccess = true root := c.Param("root") f = u.OpenFileSystem(ctx) @@ -65,7 +66,7 @@ func (h *handler) HandleRequest(c *gin.Context) { c.AbortWithStatus(http.StatusNotFound) return } - } else if !errors.Is(err, core.ErrCredentialsInvalid) { + } else if !errors.Is(err, auth.ErrCredentialsInvalid) { panic(err) } } diff --git a/server/internal/auth/auth.go b/server/internal/auth/auth.go new file mode 100644 index 00000000..2ad011fa --- /dev/null +++ b/server/internal/auth/auth.go @@ -0,0 +1,151 @@ +package auth + +import ( + "context" + "errors" + "strings" + "time" + + "codeberg.org/shroff/phylum/server/internal/auth/crypt" + "codeberg.org/shroff/phylum/server/internal/core" + "codeberg.org/shroff/phylum/server/internal/db" + "codeberg.org/shroff/phylum/server/internal/rand" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" +) + +var Cfg Config + +const accessTokenLength = 16 +const resetTokenLength = 64 +const resetTokenDuration = 10 * time.Minute + +var accessTokenValidity = pgtype.Interval{ + Days: 30, + Valid: true, +} + +var ErrCredentialsInvalid = errors.New("invalid credentials") + +func VerifyUserPassword(ctx context.Context, email, password string) (core.User, error) { + if user, passwordHash, err := userPasswordHashByEmail(db.Get(ctx), email); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return core.User{}, ErrCredentialsInvalid + } + return core.User{}, err + } else if passwordHash == "" { + return core.User{}, ErrCredentialsInvalid + } else { + if b, err := crypt.Verify(password, passwordHash); err != nil { + return core.User{}, err + } else if !b { + return core.User{}, ErrCredentialsInvalid + } + return user, nil + } +} + +func CreateAccessToken(ctx context.Context, user core.User) (string, error) { + const q = `INSERT INTO access_tokens(id, expires, user_id) VALUES ($1::TEXT, NOW() + $2::INTERVAL, $3::INT)` + id := rand.GenerateRandomString(accessTokenLength) + if _, err := db.Get(ctx).Exec(q, id, accessTokenValidity, user.ID); err != nil { + return "", err + } else { + return id, nil + } +} + +func ReadAccessToken(ctx context.Context, accessToken string) (user core.User, err error) { + const q = `SELECT t.expires, u.id, u.email, u.name, u.permissions, u.home FROM access_tokens t JOIN users u ON t.user_id = u.id WHERE t.id = $1; ` + row := db.Get(ctx).QueryRow(q, accessToken) + + var expires pgtype.Timestamp + err = row.Scan(&expires, &user.ID, &user.Email, &user.Name, &user.Permissions, &user.Home) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + err = ErrCredentialsInvalid + } + } else if time.Now().After(expires.Time) { + return core.User{}, ErrCredentialsInvalid + } + return +} + +func CreateResetToken(db db.Handler, user core.User) (string, error) { + const q = `INSERT INTO reset_tokens(user_id, token, expires) +VALUES (@user_id::INT, @token::TEXT, @expires::TIMESTAMP) +ON CONFLICT(user_id) DO UPDATE SET token = @token::TEXT, expires = @expires::TIMESTAMP` + token := rand.GenerateRandomString(resetTokenLength) + + args := pgx.NamedArgs{ + "user_id": user.ID, + "token": token, + "expires": time.Now().Add(resetTokenDuration), + } + if _, err := db.Exec(q, args); err != nil { + return "", err + } + return token, nil +} + +func ResetUserPassword(d db.Handler, user core.User, token, password string) error { + const q = `DELETE FROM reset_tokens WHERE user_id = @user_id::INT AND token = @token::TEXT RETURNING expires` + args := pgx.NamedArgs{ + "user_id": user.ID, + "token": token, + "expires": time.Now().Add(resetTokenDuration), + } + return d.RunInTx(func(d db.Handler) error { + + // UpdateUserPassword will ensure the password strength + // Not incorrect to do this before token verification because we are in a transaction. + // TODO: Are there perf implications for this in case of malicious actors? + err := UpdateUserPassword(d, user, password) + if err != nil { + return err + } + row := d.QueryRow(q, args) + var expires pgtype.Timestamp + if err := row.Scan(&expires); err == nil { + if time.Now().After(expires.Time) { + return ErrCredentialsInvalid + } + return nil + } else if errors.Is(err, pgx.ErrNoRows) { + return ErrCredentialsInvalid + } else { + return err + } + }) +} + +func UpdateUserPassword(db db.Handler, user core.User, password string) error { + if err := checkPasswordStrength(password); err != nil { + return err + } + const q = "UPDATE users SET password_hash = $2::TEXT, modified = NOW() WHERE id = $1::INT" + if hash, err := crypt.Generate(password); err != nil { + return err + } else { + if _, err := db.Exec(q, user.ID, hash); err != nil { + return err + } + } + return nil +} + +func userPasswordHashByEmail(db db.Handler, email string) (user core.User, passwordHash string, err error) { + const q = "SELECT id, email, name, home, permissions, password_hash FROM users WHERE email = $1" + row := db.QueryRow(q, strings.ToLower(email)) + err = row.Scan( + &user.ID, + &user.Email, + &user.Name, + &user.Home, + &user.Permissions, + &passwordHash) + if errors.Is(err, pgx.ErrNoRows) { + err = ErrCredentialsInvalid + } + return +} diff --git a/server/internal/auth/conifg.go b/server/internal/auth/conifg.go new file mode 100644 index 00000000..04ab3d24 --- /dev/null +++ b/server/internal/auth/conifg.go @@ -0,0 +1,21 @@ +package auth + +import "codeberg.org/shroff/phylum/server/internal/auth/crypt" + +type Config struct { + Password PasswordConfig `koanf:"password"` +} + +type PasswordConfig struct { + Backend string `koanf:"backend" ` + Crypt crypt.Config `koanf:"crypt"` + Requirements PasswordRequirements `koanf:"requirements"` +} + +type PasswordRequirements struct { + Length int `koanf:"length"` + Lower int `koanf:"lower"` + Upper int `koanf:"upper"` + Numeric int `koanf:"numeric"` + Symbols int `koanf:"symbols"` +} diff --git a/server/internal/crypt/argon2.go b/server/internal/auth/crypt/argon2.go similarity index 100% rename from server/internal/crypt/argon2.go rename to server/internal/auth/crypt/argon2.go diff --git a/server/internal/crypt/config.go b/server/internal/auth/crypt/config.go similarity index 100% rename from server/internal/crypt/config.go rename to server/internal/auth/crypt/config.go diff --git a/server/internal/crypt/crypt.go b/server/internal/auth/crypt/crypt.go similarity index 100% rename from server/internal/crypt/crypt.go rename to server/internal/auth/crypt/crypt.go diff --git a/server/internal/auth/strength.go b/server/internal/auth/strength.go new file mode 100644 index 00000000..c6fe8c47 --- /dev/null +++ b/server/internal/auth/strength.go @@ -0,0 +1,64 @@ +package auth + +import ( + "strconv" +) + +type charType int + +const ( + charTypeOther charType = iota + charTypeLower + charTypeUpper + charTypeNumeric + charTypeSymbol +) + +var charTypes = map[rune]charType{} + +type PasswordStrengthError struct { + Reason string +} + +func (e PasswordStrengthError) Error() string { + return e.Reason +} + +func init() { + for _, c := range "abcdefghijklmnopqrstuvwxyz" { + charTypes[c] = charTypeLower + } + for _, c := range "ABCDEFGHIJKLMNOPQRSTUVWXYZ" { + charTypes[c] = charTypeUpper + } + for _, c := range "0123456789" { + charTypes[c] = charTypeNumeric + } + for _, c := range "`~!@#$%^&*()-_=+[]{}\\|;:'\",.<>/?" { + charTypes[c] = charTypeSymbol + } +} + +func checkPasswordStrength(password string) error { + if len(password) < Cfg.Password.Requirements.Length { + return &PasswordStrengthError{Reason: "password must be at least " + strconv.Itoa(Cfg.Password.Requirements.Length) + " characters long."} + } + count := map[charType]int{} + for _, c := range password { + count[charTypes[c]]++ + } + + if count[charTypeLower] < Cfg.Password.Requirements.Lower { + return &PasswordStrengthError{Reason: "password must have at least " + strconv.Itoa(Cfg.Password.Requirements.Lower) + " lower case."} + } + if count[charTypeUpper] < Cfg.Password.Requirements.Upper { + return &PasswordStrengthError{Reason: "password must have at least " + strconv.Itoa(Cfg.Password.Requirements.Upper) + " upper case."} + } + if count[charTypeNumeric] < Cfg.Password.Requirements.Numeric { + return &PasswordStrengthError{Reason: "password must have at least " + strconv.Itoa(Cfg.Password.Requirements.Numeric) + " numeric."} + } + if count[charTypeSymbol] < Cfg.Password.Requirements.Symbols { + return &PasswordStrengthError{"password must have at least " + strconv.Itoa(Cfg.Password.Requirements.Symbols) + " symbols."} + } + return nil +} diff --git a/server/internal/command/admin/user/passwd.go b/server/internal/command/admin/user/passwd.go index 0ddc4e7c..af45284d 100644 --- a/server/internal/command/admin/user/passwd.go +++ b/server/internal/command/admin/user/passwd.go @@ -6,7 +6,9 @@ import ( "os" "syscall" + "codeberg.org/shroff/phylum/server/internal/auth" "codeberg.org/shroff/phylum/server/internal/core" + "codeberg.org/shroff/phylum/server/internal/db" "github.com/spf13/cobra" "golang.org/x/term" ) @@ -53,7 +55,7 @@ func setupPasswdCommand() *cobra.Command { } } - err = core.UserManagerFromContext(context.Background()).UpdateUserPassword(u, password) + err = auth.UpdateUserPassword(db.Get(context.Background()), u, password) if err != nil { fmt.Println("could not add user: " + err.Error()) os.Exit(1) diff --git a/server/internal/command/admin/user/pwreset.go b/server/internal/command/admin/user/pwreset.go index 9b34ef4a..94104002 100644 --- a/server/internal/command/admin/user/pwreset.go +++ b/server/internal/command/admin/user/pwreset.go @@ -5,7 +5,9 @@ import ( "fmt" "os" + "codeberg.org/shroff/phylum/server/internal/auth" "codeberg.org/shroff/phylum/server/internal/core" + "codeberg.org/shroff/phylum/server/internal/db" "codeberg.org/shroff/phylum/server/internal/mail" "github.com/spf13/cobra" ) @@ -22,7 +24,7 @@ func setupPwresetResetCommand() *cobra.Command { if user, err := manager.UserByEmail(email); err != nil { fmt.Println("unable to find user" + email + ": " + err.Error()) os.Exit(1) - } else if token, err := manager.CreateResetToken(user); err != nil { + } else if token, err := auth.CreateResetToken(db.Get(context.Background()), user); err != nil { fmt.Println("unable to create reset token: " + err.Error()) os.Exit(1) } else { diff --git a/server/internal/command/command.go b/server/internal/command/command.go index 396389d5..af6a3d46 100644 --- a/server/internal/command/command.go +++ b/server/internal/command/command.go @@ -8,12 +8,13 @@ import ( "path" "strings" + "codeberg.org/shroff/phylum/server/internal/auth" + "codeberg.org/shroff/phylum/server/internal/auth/crypt" "codeberg.org/shroff/phylum/server/internal/command/admin" "codeberg.org/shroff/phylum/server/internal/command/fs" "codeberg.org/shroff/phylum/server/internal/command/serve" "codeberg.org/shroff/phylum/server/internal/command/user" "codeberg.org/shroff/phylum/server/internal/core" - "codeberg.org/shroff/phylum/server/internal/crypt" "codeberg.org/shroff/phylum/server/internal/db" "codeberg.org/shroff/phylum/server/internal/mail" "codeberg.org/shroff/phylum/server/internal/storage" @@ -117,7 +118,9 @@ func SetupCommand() { serve.Cfg = cfg.Server mail.Cfg = cfg.Mail core.Cfg = cfg.User - crypt.Cfg = cfg.Crypt + auth.Cfg = cfg.Auth + crypt.Cfg = cfg.Auth.Password.Crypt + if err := storage.Initialize(db.Get(context.Background())); err != nil { logrus.Fatal("Failed to initialize storage: " + err.Error()) } diff --git a/server/internal/command/config.defaults.yml b/server/internal/command/config.defaults.yml index b52edded..ef9ff272 100644 --- a/server/internal/command/config.defaults.yml +++ b/server/internal/command/config.defaults.yml @@ -13,22 +13,26 @@ storage: user: password: - length: 12 - lower: 1 - upper: 1 - numeric: 1 - symbols: 1 basedir: /home permission: 0x10 # Invite users -crypt: - hash: argon2 - argon2: - memory: 2048 - iterations: 6 - parallelism: 4 - salt: 32 - key: 32 +auth: + password: + backend: crypt + crypt: + hash: argon2 + argon2: + memory: 2048 + iterations: 6 + parallelism: 4 + salt: 32 + key: 32 + requirements: + length: 12 + lower: 1 + upper: 1 + numeric: 1 + symbols: 1 server: host: diff --git a/server/internal/command/config.go b/server/internal/command/config.go index a87c5f8d..a313923e 100644 --- a/server/internal/command/config.go +++ b/server/internal/command/config.go @@ -1,9 +1,9 @@ package command import ( + "codeberg.org/shroff/phylum/server/internal/auth" "codeberg.org/shroff/phylum/server/internal/command/serve" "codeberg.org/shroff/phylum/server/internal/core" - "codeberg.org/shroff/phylum/server/internal/crypt" "codeberg.org/shroff/phylum/server/internal/db" "codeberg.org/shroff/phylum/server/internal/mail" "codeberg.org/shroff/phylum/server/internal/storage" @@ -16,5 +16,5 @@ type Config struct { Server serve.Config `koanf:"server"` Mail mail.Config `koanf:"mail"` User core.Config `koanf:"user"` - Crypt crypt.Config `koanf:"crypt"` + Auth auth.Config `koanf:"auth"` } diff --git a/server/internal/core/config.go b/server/internal/core/config.go index e6301ed1..d5bf5eee 100644 --- a/server/internal/core/config.go +++ b/server/internal/core/config.go @@ -1,15 +1,6 @@ package core type Config struct { - Password PasswordConfig `koanf:"password"` BaseDir string `koanf:"basedir"` Permisison UserPermissions `koanf:"permission"` } - -type PasswordConfig struct { - Length int `koanf:"length"` - Lower int `koanf:"lower"` - Upper int `koanf:"upper"` - Numeric int `koanf:"numeric"` - Symbols int `koanf:"symbols"` -} diff --git a/server/internal/core/core.go b/server/internal/core/core.go index 0b0ac9df..d6bd44cd 100644 --- a/server/internal/core/core.go +++ b/server/internal/core/core.go @@ -91,17 +91,9 @@ type UserManager interface { UserEmailByID(id int) (string, error) UserHome(email string) (pgtype.UUID, error) - // user_auth.go - VerifyUserPassword(email, password string) (User, error) - CreateAccessToken(user User) (string, error) - ReadAccessToken(accessToken string) (User, error) - CreateResetToken(user User) (string, error) - ResetUserPassword(user User, token, password string) error - // user_update.go UpdateUserHome(user User, home pgtype.UUID) error UpdateUserName(user User, name string) error - UpdateUserPassword(user User, password string) error GrantUserPermissions(user User, permissions UserPermissions) error RevokeUserPermissions(user User, permissions UserPermissions) error diff --git a/server/internal/core/filesystem_readonly.go b/server/internal/core/filesystem_readonly.go index db00713f..46c612a1 100644 --- a/server/internal/core/filesystem_readonly.go +++ b/server/internal/core/filesystem_readonly.go @@ -5,7 +5,7 @@ import ( "errors" "time" - "codeberg.org/shroff/phylum/server/internal/crypt" + "codeberg.org/shroff/phylum/server/internal/auth/crypt" "codeberg.org/shroff/phylum/server/internal/db" "github.com/google/uuid" "github.com/jackc/pgx/v5" diff --git a/server/internal/core/password.go b/server/internal/core/password.go deleted file mode 100644 index ef9a8429..00000000 --- a/server/internal/core/password.go +++ /dev/null @@ -1,57 +0,0 @@ -package core - -import ( - "net/http" - "strconv" -) - -type charType int - -const ( - charTypeOther charType = iota - charTypeLower - charTypeUpper - charTypeNumeric - charTypeSymbol -) - -var charTypes = map[rune]charType{} - -func init() { - for _, c := range "abcdefghijklmnopqrstuvwxyz" { - charTypes[c] = charTypeLower - } - for _, c := range "ABCDEFGHIJKLMNOPQRSTUVWXYZ" { - charTypes[c] = charTypeUpper - } - for _, c := range "0123456789" { - charTypes[c] = charTypeNumeric - } - for _, c := range "`~!@#$%^&*()-_=+[]{}\\|;:'\",.<>/?" { - charTypes[c] = charTypeSymbol - } -} - -func checkPasswordStrength(password string) error { - if len(password) < Cfg.Password.Length { - return NewError(http.StatusBadRequest, "password_invalid", "Must be at least "+strconv.Itoa(Cfg.Password.Length)+" characters long.") - } - count := map[charType]int{} - for _, c := range password { - count[charTypes[c]]++ - } - - if count[charTypeLower] < Cfg.Password.Lower { - return NewError(http.StatusBadRequest, "password_invalid", "Must have at least "+strconv.Itoa(Cfg.Password.Lower)+" lower case.") - } - if count[charTypeUpper] < Cfg.Password.Upper { - return NewError(http.StatusBadRequest, "password_invalid", "Must have at least "+strconv.Itoa(Cfg.Password.Upper)+" upper case.") - } - if count[charTypeNumeric] < Cfg.Password.Numeric { - return NewError(http.StatusBadRequest, "password_invalid", "Must have at least "+strconv.Itoa(Cfg.Password.Numeric)+" numeric.") - } - if count[charTypeSymbol] < Cfg.Password.Symbols { - return NewError(http.StatusBadRequest, "password_invalid", "Must have at least "+strconv.Itoa(Cfg.Password.Symbols)+" symbols.") - } - return nil -} diff --git a/server/internal/core/resource_publink.go b/server/internal/core/resource_publink.go index 2bc3f490..d8199493 100644 --- a/server/internal/core/resource_publink.go +++ b/server/internal/core/resource_publink.go @@ -3,7 +3,7 @@ package core import ( "strings" - "codeberg.org/shroff/phylum/server/internal/crypt" + "codeberg.org/shroff/phylum/server/internal/auth/crypt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) diff --git a/server/internal/core/user_auth.go b/server/internal/core/user_auth.go deleted file mode 100644 index 39884c4b..00000000 --- a/server/internal/core/user_auth.go +++ /dev/null @@ -1,118 +0,0 @@ -package core - -import ( - "net/http" - "time" - - "codeberg.org/shroff/phylum/server/internal/crypt" - "codeberg.org/shroff/phylum/server/internal/db" - "codeberg.org/shroff/phylum/server/internal/rand" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgtype" -) - -const accessTokenLength = 16 -const resetTokenLength = 64 -const resetTokenDuration = 10 * time.Minute - -var accessTokenValidity = pgtype.Interval{ - Days: 30, - Valid: true, -} - -var ErrCredentialsInvalid = NewError(http.StatusUnauthorized, "credentials_invalid", "invalid Credentials") -var errTokenNotFound = NewError(http.StatusBadRequest, "token_not_found", "Token Not Found") - -func (m manager) VerifyUserPassword(email, password string) (User, error) { - if user, passwordHash, err := m.userPasswordHashByEmail(email); err != nil { - if Is(err, pgx.ErrNoRows) { - return User{}, ErrCredentialsInvalid - } - return User{}, err - } else if passwordHash == "" { - return User{}, ErrCredentialsInvalid - } else { - if b, err := crypt.Verify(password, passwordHash); err != nil { - return User{}, err - } else if !b { - return User{}, ErrCredentialsInvalid - } - return user, nil - } -} - -func (m manager) CreateAccessToken(user User) (string, error) { - const q = `INSERT INTO access_tokens(id, expires, user_id) VALUES ($1::TEXT, NOW() + $2::INTERVAL, $3::INT)` - id := rand.GenerateRandomString(accessTokenLength) - if _, err := m.db.Exec(q, id, accessTokenValidity, user.ID); err != nil { - return "", err - } else { - return id, nil - } -} - -func (m manager) ReadAccessToken(accessToken string) (user User, err error) { - const q = `SELECT t.expires, u.id, u.email, u.name, u.permissions, u.home FROM access_tokens t JOIN users u ON t.user_id = u.id WHERE t.id = $1; ` - row := m.db.QueryRow(q, accessToken) - - var expires pgtype.Timestamp - err = row.Scan(&expires, &user.ID, &user.Email, &user.Name, &user.Permissions, &user.Home) - if err != nil { - if Is(err, pgx.ErrNoRows) { - err = ErrCredentialsInvalid - } - } else if time.Now().After(expires.Time) { - return User{}, ErrCredentialsInvalid - } - return -} - -func (m manager) CreateResetToken(user User) (string, error) { - const q = `INSERT INTO reset_tokens(user_id, token, expires) -VALUES (@user_id::INT, @token::TEXT, @expires::TIMESTAMP) -ON CONFLICT(user_id) DO UPDATE SET token = @token::TEXT, expires = @expires::TIMESTAMP` - token := rand.GenerateRandomString(resetTokenLength) - - args := pgx.NamedArgs{ - "user_id": user.ID, - "token": token, - "expires": time.Now().Add(resetTokenDuration), - } - if _, err := m.db.Exec(q, args); err != nil { - return "", err - } - return token, nil -} - -func (m manager) ResetUserPassword(user User, token, password string) error { - const q = `DELETE FROM reset_tokens WHERE user_id = @user_id::INT AND token = @token::TEXT RETURNING expires` - args := pgx.NamedArgs{ - "user_id": user.ID, - "token": token, - "expires": time.Now().Add(resetTokenDuration), - } - return m.db.RunInTx(func(d db.Handler) error { - m := m.withDb(d) - - // UpdateUserPassword will ensure the password strength - // Not incorrect to do this before token verification because we are in a transaction. - // TODO: Are there perf implications for this in case of malicious actors? - err := m.UpdateUserPassword(user, password) - if err != nil { - return err - } - row := m.db.QueryRow(q, args) - var expires pgtype.Timestamp - if err := row.Scan(&expires); err == nil { - if time.Now().After(expires.Time) { - return errTokenNotFound - } - return nil - } else if Is(err, pgx.ErrNoRows) { - return errTokenNotFound - } else { - return err - } - }) - -} diff --git a/server/internal/core/user_manager.go b/server/internal/core/user_manager.go index 608477bd..2c905ece 100644 --- a/server/internal/core/user_manager.go +++ b/server/internal/core/user_manager.go @@ -137,19 +137,3 @@ RETURNING id, email, name, home, permissions` return user, err } } - -func (m manager) userPasswordHashByEmail(email string) (user User, passwordHash string, err error) { - const q = "SELECT id, email, name, home, permissions, password_hash FROM users WHERE email = $1" - row := m.db.QueryRow(q, strings.ToLower(email)) - err = row.Scan( - &user.ID, - &user.Email, - &user.Name, - &user.Home, - &user.Permissions, - &passwordHash) - if Is(err, pgx.ErrNoRows) { - err = ErrUserNotFound - } - return -} diff --git a/server/internal/core/user_update.go b/server/internal/core/user_update.go index 1b5b3424..5cc87b85 100644 --- a/server/internal/core/user_update.go +++ b/server/internal/core/user_update.go @@ -1,7 +1,6 @@ package core import ( - "codeberg.org/shroff/phylum/server/internal/crypt" "github.com/jackc/pgx/v5/pgtype" ) @@ -21,22 +20,6 @@ func (m manager) UpdateUserName(user User, name string) error { return nil } -func (m manager) UpdateUserPassword(user User, password string) error { - if err := checkPasswordStrength(password); err != nil { - return err - } - - const q = "UPDATE users SET password_hash = $2::TEXT, modified = NOW() WHERE id = $1::INT" - if hash, err := crypt.Generate(password); err != nil { - return err - } else { - if _, err := m.db.Exec(q, user.ID, hash); err != nil { - return err - } - } - return nil -} - func (m manager) GrantUserPermissions(user User, permissions UserPermissions) error { const q = "UPDATE users SET permissions = permissions | $2::INTEGER, modified = NOW() WHERE id = $1::INT" if _, err := m.db.Exec(q, user.ID, permissions); err != nil {