Support auto-hashing fields via hooks.
This commit is contained in:
parent
56f8a619a5
commit
7ff1e3f9d2
6 changed files with 49 additions and 36 deletions
|
|
@ -1,11 +1,15 @@
|
||||||
package schema
|
package schema
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"entgo.io/ent"
|
"entgo.io/ent"
|
||||||
"entgo.io/ent/schema/edge"
|
"entgo.io/ent/schema/edge"
|
||||||
"entgo.io/ent/schema/field"
|
"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.
|
// PasswordToken holds the schema definition for the PasswordToken entity.
|
||||||
|
|
@ -34,3 +38,25 @@ func (PasswordToken) Edges() []ent.Edge {
|
||||||
Unique(),
|
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,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
|
|
||||||
ge "github.com/mikestefanello/pagoda/ent"
|
ge "github.com/mikestefanello/pagoda/ent"
|
||||||
"github.com/mikestefanello/pagoda/ent/hook"
|
"github.com/mikestefanello/pagoda/ent/hook"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
"entgo.io/ent"
|
"entgo.io/ent"
|
||||||
"entgo.io/ent/schema/edge"
|
"entgo.io/ent/schema/edge"
|
||||||
|
|
@ -59,6 +60,14 @@ func (User) Hooks() []ent.Hook {
|
||||||
if v, exists := m.Email(); exists {
|
if v, exists := m.Email(); exists {
|
||||||
m.SetEmail(strings.ToLower(v))
|
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)
|
return next.Mutate(ctx, m)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -206,18 +206,12 @@ func (h *Auth) RegisterSubmit(ctx echo.Context) error {
|
||||||
return err
|
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.
|
// Attempt creating the user.
|
||||||
u, err := h.orm.User.
|
u, err := h.orm.User.
|
||||||
Create().
|
Create().
|
||||||
SetName(input.Name).
|
SetName(input.Name).
|
||||||
SetEmail(input.Email).
|
SetEmail(input.Email).
|
||||||
SetPassword(pwHash).
|
SetPassword(input.Password).
|
||||||
Save(ctx.Request().Context())
|
Save(ctx.Request().Context())
|
||||||
|
|
||||||
switch err.(type) {
|
switch err.(type) {
|
||||||
|
|
@ -305,19 +299,13 @@ func (h *Auth) ResetPasswordSubmit(ctx echo.Context) error {
|
||||||
return err
|
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.
|
// Get the requesting user.
|
||||||
usr := ctx.Get(context.UserKey).(*ent.User)
|
usr := ctx.Get(context.UserKey).(*ent.User)
|
||||||
|
|
||||||
// Update the user.
|
// Update the user.
|
||||||
_, err = usr.
|
_, err = usr.
|
||||||
Update().
|
Update().
|
||||||
SetPassword(hash).
|
SetPassword(input.Password).
|
||||||
Save(ctx.Request().Context())
|
Save(ctx.Request().Context())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -106,15 +106,6 @@ func (c *AuthClient) GetAuthenticatedUser(ctx echo.Context) (*ent.User, error) {
|
||||||
return nil, NotAuthenticatedError{}
|
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
|
// CheckPassword check if a given password matches a given hash
|
||||||
func (c *AuthClient) CheckPassword(password, hash string) error {
|
func (c *AuthClient) CheckPassword(password, hash string) error {
|
||||||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
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.
|
// GeneratePasswordResetToken generates a password reset token for a given user.
|
||||||
// For security purposes, the token itself is not stored in the database but rather
|
// 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
|
// 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) {
|
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
|
// Generate the token, which is what will go in the URL, but not the database
|
||||||
token, err := c.RandomToken(c.config.App.PasswordToken.Length)
|
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
|
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
|
// Create and save the password reset token
|
||||||
pt, err := c.orm.PasswordToken.
|
pt, err := c.orm.PasswordToken.
|
||||||
Create().
|
Create().
|
||||||
SetHash(hash).
|
SetHash(token).
|
||||||
SetUserID(userID).
|
SetUserID(userID).
|
||||||
Save(ctx.Request().Context())
|
Save(ctx.Request().Context())
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
|
|
||||||
"github.com/mikestefanello/pagoda/ent/passwordtoken"
|
"github.com/mikestefanello/pagoda/ent/passwordtoken"
|
||||||
"github.com/mikestefanello/pagoda/ent/user"
|
"github.com/mikestefanello/pagoda/ent/user"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
|
@ -41,12 +42,12 @@ func TestAuthClient_Auth(t *testing.T) {
|
||||||
assertNoAuth()
|
assertNoAuth()
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthClient_PasswordHashing(t *testing.T) {
|
func TestAuthClient_CheckPassword(t *testing.T) {
|
||||||
pw := "testcheckpassword"
|
pw := "testcheckpassword"
|
||||||
hash, err := c.Auth.HashPassword(pw)
|
hash, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotEqual(t, hash, pw)
|
assert.NotEqual(t, hash, pw)
|
||||||
err = c.Auth.CheckPassword(pw, hash)
|
err = c.Auth.CheckPassword(pw, string(hash))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,9 +71,13 @@ func AdminEntityForm(ctx echo.Context, schema *load.Schema, values url.Values) e
|
||||||
// TODO password?
|
// TODO password?
|
||||||
switch f.Info.Type {
|
switch f.Info.Type {
|
||||||
case field.TypeString:
|
case field.TypeString:
|
||||||
|
inputType := "text"
|
||||||
|
if f.Sensitive {
|
||||||
|
inputType = "password"
|
||||||
|
}
|
||||||
nodes = append(nodes, InputField(InputFieldParams{
|
nodes = append(nodes, InputField(InputFieldParams{
|
||||||
Name: f.Name,
|
Name: f.Name,
|
||||||
InputType: "text",
|
InputType: inputType,
|
||||||
Label: label(f.Name),
|
Label: label(f.Name),
|
||||||
Value: getValue(f.Name),
|
Value: getValue(f.Name),
|
||||||
}))
|
}))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue