From 7ff1e3f9d2c55a05d7470b60581ba3c0b575b49a Mon Sep 17 00:00:00 2001 From: mikestefanello <552328+mikestefanello@users.noreply.github.com> Date: Sat, 12 Apr 2025 16:19:18 -0400 Subject: [PATCH] Support auto-hashing fields via hooks. --- ent/schema/passwordtoken.go | 26 ++++++++++++++++++++++++++ ent/schema/user.go | 9 +++++++++ pkg/handlers/auth.go | 16 ++-------------- pkg/services/auth.go | 19 ++----------------- pkg/services/auth_test.go | 9 +++++---- pkg/ui/pages/entity.go | 6 +++++- 6 files changed, 49 insertions(+), 36 deletions(-) diff --git a/ent/schema/passwordtoken.go b/ent/schema/passwordtoken.go index 5a59f33..81195af 100644 --- a/ent/schema/passwordtoken.go +++ b/ent/schema/passwordtoken.go @@ -1,11 +1,15 @@ package schema import ( + "context" "time" "entgo.io/ent" "entgo.io/ent/schema/edge" "entgo.io/ent/schema/field" + ge "github.com/mikestefanello/pagoda/ent" + "github.com/mikestefanello/pagoda/ent/hook" + "golang.org/x/crypto/bcrypt" ) // PasswordToken holds the schema definition for the PasswordToken entity. @@ -34,3 +38,25 @@ func (PasswordToken) Edges() []ent.Edge { Unique(), } } + +// Hooks of the PasswordToken. +func (PasswordToken) Hooks() []ent.Hook { + return []ent.Hook{ + hook.On( + func(next ent.Mutator) ent.Mutator { + return hook.PasswordTokenFunc(func(ctx context.Context, m *ge.PasswordTokenMutation) (ent.Value, error) { + if v, exists := m.Hash(); exists { + hash, err := bcrypt.GenerateFromPassword([]byte(v), bcrypt.DefaultCost) + if err != nil { + return "", err + } + m.SetHash(string(hash)) + } + return next.Mutate(ctx, m) + }) + }, + // Limit the hook only for these operations. + ent.OpCreate|ent.OpUpdate|ent.OpUpdateOne, + ), + } +} diff --git a/ent/schema/user.go b/ent/schema/user.go index 13f9ff7..5e624b0 100644 --- a/ent/schema/user.go +++ b/ent/schema/user.go @@ -8,6 +8,7 @@ import ( ge "github.com/mikestefanello/pagoda/ent" "github.com/mikestefanello/pagoda/ent/hook" + "golang.org/x/crypto/bcrypt" "entgo.io/ent" "entgo.io/ent/schema/edge" @@ -59,6 +60,14 @@ func (User) Hooks() []ent.Hook { if v, exists := m.Email(); exists { m.SetEmail(strings.ToLower(v)) } + + if v, exists := m.Password(); exists { + hash, err := bcrypt.GenerateFromPassword([]byte(v), bcrypt.DefaultCost) + if err != nil { + return "", err + } + m.SetPassword(string(hash)) + } return next.Mutate(ctx, m) }) }, diff --git a/pkg/handlers/auth.go b/pkg/handlers/auth.go index a96ce8f..707fe10 100644 --- a/pkg/handlers/auth.go +++ b/pkg/handlers/auth.go @@ -206,18 +206,12 @@ func (h *Auth) RegisterSubmit(ctx echo.Context) error { return err } - // Hash the password. - pwHash, err := h.auth.HashPassword(input.Password) - if err != nil { - return fail(err, "unable to hash password") - } - // Attempt creating the user. u, err := h.orm.User. Create(). SetName(input.Name). SetEmail(input.Email). - SetPassword(pwHash). + SetPassword(input.Password). Save(ctx.Request().Context()) switch err.(type) { @@ -305,19 +299,13 @@ func (h *Auth) ResetPasswordSubmit(ctx echo.Context) error { return err } - // Hash the new password. - hash, err := h.auth.HashPassword(input.Password) - if err != nil { - return fail(err, "unable to hash password") - } - // Get the requesting user. usr := ctx.Get(context.UserKey).(*ent.User) // Update the user. _, err = usr. Update(). - SetPassword(hash). + SetPassword(input.Password). Save(ctx.Request().Context()) if err != nil { diff --git a/pkg/services/auth.go b/pkg/services/auth.go index b25ebf9..4aa98bf 100644 --- a/pkg/services/auth.go +++ b/pkg/services/auth.go @@ -106,15 +106,6 @@ func (c *AuthClient) GetAuthenticatedUser(ctx echo.Context) (*ent.User, error) { return nil, NotAuthenticatedError{} } -// HashPassword returns a hash of a given password -func (c *AuthClient) HashPassword(password string) (string, error) { - hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - if err != nil { - return "", err - } - return string(hash), nil -} - // CheckPassword check if a given password matches a given hash func (c *AuthClient) CheckPassword(password, hash string) error { return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) @@ -123,7 +114,7 @@ func (c *AuthClient) CheckPassword(password, hash string) error { // GeneratePasswordResetToken generates a password reset token for a given user. // For security purposes, the token itself is not stored in the database but rather // a hash of the token, exactly how passwords are handled. This method returns both -// the generated token as well as the token entity which only contains the hash. +// the generated token and the token entity which only contains the hash. func (c *AuthClient) GeneratePasswordResetToken(ctx echo.Context, userID int) (string, *ent.PasswordToken, error) { // Generate the token, which is what will go in the URL, but not the database token, err := c.RandomToken(c.config.App.PasswordToken.Length) @@ -131,16 +122,10 @@ func (c *AuthClient) GeneratePasswordResetToken(ctx echo.Context, userID int) (s return "", nil, err } - // Hash the token, which is what will be stored in the database - hash, err := c.HashPassword(token) - if err != nil { - return "", nil, err - } - // Create and save the password reset token pt, err := c.orm.PasswordToken. Create(). - SetHash(hash). + SetHash(token). SetUserID(userID). Save(ctx.Request().Context()) diff --git a/pkg/services/auth_test.go b/pkg/services/auth_test.go index 4028073..b654296 100644 --- a/pkg/services/auth_test.go +++ b/pkg/services/auth_test.go @@ -8,6 +8,7 @@ import ( "github.com/mikestefanello/pagoda/ent/passwordtoken" "github.com/mikestefanello/pagoda/ent/user" + "golang.org/x/crypto/bcrypt" "github.com/stretchr/testify/require" @@ -41,12 +42,12 @@ func TestAuthClient_Auth(t *testing.T) { assertNoAuth() } -func TestAuthClient_PasswordHashing(t *testing.T) { +func TestAuthClient_CheckPassword(t *testing.T) { pw := "testcheckpassword" - hash, err := c.Auth.HashPassword(pw) - assert.NoError(t, err) + hash, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost) + require.NoError(t, err) assert.NotEqual(t, hash, pw) - err = c.Auth.CheckPassword(pw, hash) + err = c.Auth.CheckPassword(pw, string(hash)) assert.NoError(t, err) } diff --git a/pkg/ui/pages/entity.go b/pkg/ui/pages/entity.go index a0c2b2d..cdbf7fd 100644 --- a/pkg/ui/pages/entity.go +++ b/pkg/ui/pages/entity.go @@ -71,9 +71,13 @@ func AdminEntityForm(ctx echo.Context, schema *load.Schema, values url.Values) e // TODO password? switch f.Info.Type { case field.TypeString: + inputType := "text" + if f.Sensitive { + inputType = "password" + } nodes = append(nodes, InputField(InputFieldParams{ Name: f.Name, - InputType: "text", + InputType: inputType, Label: label(f.Name), Value: getValue(f.Name), }))