From 6b340d7195c17275703f5875459390d7e9950558 Mon Sep 17 00:00:00 2001 From: nochill Date: Fri, 8 Sep 2023 22:25:22 +0700 Subject: [PATCH] util for password and configs --- util/config.go | 32 +++++++++++++++++++++++ util/password.go | 19 ++++++++++++++ util/password_test.go | 23 +++++++++++++++++ util/random.go | 41 ++++++++++++++++++++++++++++++ util/token/maker.go | 8 ++++++ util/token/paseto.go | 53 +++++++++++++++++++++++++++++++++++++++ util/token/paseto_test.go | 52 ++++++++++++++++++++++++++++++++++++++ util/token/payload.go | 36 ++++++++++++++++++++++++++ 8 files changed, 264 insertions(+) create mode 100644 util/config.go create mode 100644 util/password.go create mode 100644 util/password_test.go create mode 100644 util/random.go create mode 100644 util/token/maker.go create mode 100644 util/token/paseto.go create mode 100644 util/token/paseto_test.go create mode 100644 util/token/payload.go diff --git a/util/config.go b/util/config.go new file mode 100644 index 0000000..5edce58 --- /dev/null +++ b/util/config.go @@ -0,0 +1,32 @@ +package util + +import ( + "time" + + "github.com/spf13/viper" +) + +type Config struct { + DBDriver string `mapstructure:"DB_TYPE"` + DBSource string `mapstructure:"DB_SOURCE"` + ServerAddress string `mapstructure:"SERVER_ADDRESS"` + TokenSymmetricKey string `mapstructure:"TOKEN_SYMMETRIC_KEY"` + TokenDuration time.Duration `mapstructure:"TOKEN_DURATION"` + RefreshTokenDuration time.Duration `mapstructure:"REFRESH_TOKEN_DURATION"` +} + +func LoadConfig(path string) (config Config, err error) { + viper.AddConfigPath(path) + viper.SetConfigName("dev") + viper.SetConfigType("env") + + viper.AutomaticEnv() + + err = viper.ReadInConfig() + if err != nil { + return + } + + err = viper.Unmarshal(&config) + return +} diff --git a/util/password.go b/util/password.go new file mode 100644 index 0000000..3b7d3f5 --- /dev/null +++ b/util/password.go @@ -0,0 +1,19 @@ +package util + +import ( + "fmt" + + "golang.org/x/crypto/bcrypt" +) + +func HashPassword(password string) (string, error) { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "", fmt.Errorf("Failed to hash password: %w", err) + } + return string(hashedPassword), nil +} + +func CheckPassword(password string, hashedPassword string) error { + return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) +} diff --git a/util/password_test.go b/util/password_test.go new file mode 100644 index 0000000..c86c89b --- /dev/null +++ b/util/password_test.go @@ -0,0 +1,23 @@ +package util + +import ( + "testing" + + "github.com/stretchr/testify/require" + "golang.org/x/crypto/bcrypt" +) + +func TestPasswordUtil(t *testing.T) { + password := RandomString(10) + + hashedPassword, err := HashPassword(password) + require.NoError(t, err) + require.NotEmpty(t, hashedPassword) + + err = CheckPassword(password, hashedPassword) + require.NoError(t, err) + + wrongPassword := RandomString(5) + err = CheckPassword(wrongPassword, hashedPassword) + require.EqualError(t, err, bcrypt.ErrMismatchedHashAndPassword.Error()) +} diff --git a/util/random.go b/util/random.go new file mode 100644 index 0000000..2ab7636 --- /dev/null +++ b/util/random.go @@ -0,0 +1,41 @@ +package util + +import ( + "fmt" + "math/rand" + "strings" + "time" +) + +const alphabet = "abcdefghijklmnopqrstuvwxyz" + +func init() { + rand.Seed(time.Now().UnixNano()) +} + +// RandomInt generates a random integer between min and max +func RandomInt(min, max int64) int64 { + return min + rand.Int63n(max-min+1) +} + +// RandomString generates a random string of length n +func RandomString(n int) string { + var sb strings.Builder + k := len(alphabet) + + for i := 0; i < n; i++ { + c := alphabet[rand.Intn(k)] + sb.WriteByte(c) + } + + return sb.String() +} + +func RandomEmail() string { + return fmt.Sprintf("%s@mail.com", RandomString(5)) +} + +func RandomTransactionCode(prefix string, merchant_index int64) string { + time_now := time.Now().Unix() + return fmt.Sprintf("%s%d%d", prefix, merchant_index, time_now) +} diff --git a/util/token/maker.go b/util/token/maker.go new file mode 100644 index 0000000..7c9f103 --- /dev/null +++ b/util/token/maker.go @@ -0,0 +1,8 @@ +package token + +import "time" + +type Maker interface { + CreateToken(email string, userID int, duration time.Duration) (string, *Payload, error) + VerifyToken(token string) (*Payload, error) +} diff --git a/util/token/paseto.go b/util/token/paseto.go new file mode 100644 index 0000000..2001502 --- /dev/null +++ b/util/token/paseto.go @@ -0,0 +1,53 @@ +package token + +import ( + "fmt" + "time" + + "github.com/aead/chacha20poly1305" + "github.com/o1egl/paseto" +) + +type PasetoMaker struct { + paseto *paseto.V2 + symmetricKey []byte +} + +func NewPasetoMaker(symmetricKey string) (Maker, error) { + if len(symmetricKey) != chacha20poly1305.KeySize { + return nil, fmt.Errorf("invalid key size: must be exactly %d characters", chacha20poly1305.KeySize) + } + + maker := &PasetoMaker{ + paseto: paseto.NewV2(), + symmetricKey: []byte(symmetricKey), + } + + return maker, nil +} + +func (maker *PasetoMaker) CreateToken(email string, UserID int, duration time.Duration) (string, *Payload, error) { + payload, err := NewPayload(email, UserID, duration) + if err != nil { + return "", payload, err + } + + token, err := maker.paseto.Encrypt(maker.symmetricKey, payload, nil) + return token, payload, err +} + +func (maker *PasetoMaker) VerifyToken(token string) (*Payload, error) { + payload := &Payload{} + + err := maker.paseto.Decrypt(token, maker.symmetricKey, payload, nil) + if err != nil { + return nil, ErrInvalidToken + } + + err = payload.Valid() + if err != nil { + return nil, err + } + + return payload, nil +} diff --git a/util/token/paseto_test.go b/util/token/paseto_test.go new file mode 100644 index 0000000..529d0d2 --- /dev/null +++ b/util/token/paseto_test.go @@ -0,0 +1,52 @@ +package token + +import ( + "math/rand" + "testing" + "time" + + "git.nochill.in/nochill/hiling_go/util" + "github.com/stretchr/testify/require" +) + +var userID = rand.Intn(10) + +func TestPasetoMaker(t *testing.T) { + maker, err := NewPasetoMaker(util.RandomString(32)) + require.NoError(t, err) + + email := util.RandomEmail() + duration := time.Minute + + issuedAt := time.Now() + expiredAt := issuedAt.Add(duration) + + token, payload, err := maker.CreateToken(email, userID, duration) + require.NoError(t, err) + require.NotEmpty(t, token) + require.NotEmpty(t, payload) + + payload, err = maker.VerifyToken(token) + require.NoError(t, err) + require.NotEmpty(t, payload) + + // require.NotZero(t, payload.ID) + require.Equal(t, email, payload.Email) + require.WithinDuration(t, issuedAt, payload.IssuedAt, time.Second) + require.WithinDuration(t, expiredAt, payload.ExpiredAt, time.Second) +} + +func TestExpiredPasetoToken(t *testing.T) { + maker, err := NewPasetoMaker(util.RandomString(32)) + require.NoError(t, err) + + token, payload, err := maker.CreateToken(util.RandomEmail(), userID, -time.Minute) + require.NoError(t, err) + require.NotEmpty(t, token) + require.NotEmpty(t, payload) + + payload, err = maker.VerifyToken(token) + require.Error(t, err) + require.EqualError(t, err, ErrExpiredToken.Error()) + require.Nil(t, payload) +} diff --git a/util/token/payload.go b/util/token/payload.go new file mode 100644 index 0000000..1081c00 --- /dev/null +++ b/util/token/payload.go @@ -0,0 +1,36 @@ +package token + +import ( + "errors" + "time" +) + +var ( + ErrExpiredToken = errors.New("token has expired") + ErrInvalidToken = errors.New("token is invalid") +) + +type Payload struct { + Email string `json:"email"` + UserID int `json:"user_id"` + IssuedAt time.Time `json:"issued_at"` + ExpiredAt time.Time `json:"expired_at"` +} + +func NewPayload(email string, user_id int, duration time.Duration) (*Payload, error) { + payload := &Payload{ + Email: email, + UserID: user_id, + IssuedAt: time.Now(), + ExpiredAt: time.Now().Add(duration), + } + + return payload, nil +} + +func (payload *Payload) Valid() error { + if time.Now().After(payload.ExpiredAt) { + return ErrExpiredToken + } + return nil +}