feat: add EIP712 verification, auth service implemention and jwt gen
This commit is contained in:
@@ -14,26 +14,23 @@ type SessionRepo interface {
|
||||
GetUserSessions(ctx context.Context, userID uuid.UUID) ([]UserSession, error)
|
||||
}
|
||||
|
||||
// TODO: add IP and UserAgent
|
||||
type UserSession struct {
|
||||
ID uuid.UUID
|
||||
UserID uuid.UUID
|
||||
User User
|
||||
WalletID uuid.UUID
|
||||
IPaddress string
|
||||
Agent string
|
||||
WalletID string
|
||||
ExpiresAt time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
func NewSession(userID, walletID uuid.UUID, ipAddress, agent string, expiresAt time.Time) *UserSession {
|
||||
func NewSession(userID uuid.UUID, walletID string, expiresAt time.Time) *UserSession {
|
||||
return &UserSession{
|
||||
ID: uuid.New(),
|
||||
UserID: userID,
|
||||
WalletID: walletID,
|
||||
IPaddress: ipAddress,
|
||||
Agent: agent,
|
||||
ExpiresAt: expiresAt,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
UpdatedAt: time.Now().UTC(),
|
||||
|
||||
+57
-14
@@ -1,30 +1,33 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"backend/pkg/validate/national"
|
||||
"backend/pkg/validate/phone"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"net/mail"
|
||||
|
||||
validate "backend/pkg/validate/common"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrPhoneInvalid = errors.New("phone number is invalid")
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
ErrNationalIDInvalid = errors.New("national ID is invalid")
|
||||
ErrWalletInvalid = errors.New("wallet address is invalid")
|
||||
ErrEmailInvalid = errors.New("email is invalid")
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
ErrEmailInvalid = errors.New("email is invalid")
|
||||
)
|
||||
|
||||
type UserRepo interface {
|
||||
Create(ctx context.Context, user *User) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*User, error)
|
||||
GetByPhone(ctx context.Context, phone string) (*User, error)
|
||||
GetByPubKey(ctx context.Context, pubKey string) (*User, error)
|
||||
Update(ctx context.Context, user *User) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
}
|
||||
@@ -53,25 +56,65 @@ type Challenge struct {
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
// TODO: check EIP712 in here for challenge validation
|
||||
func (c *Challenge) IsExpired() bool {
|
||||
return time.Now().After(c.ExpiresAt)
|
||||
}
|
||||
|
||||
func (c *Challenge) Verify(address string, signedMsg string) (bool, error) {
|
||||
if c.IsExpired() {
|
||||
return false, errors.New("challenge has expired")
|
||||
}
|
||||
|
||||
signature, err := hexutil.Decode(signedMsg)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("decode signature: %w", err)
|
||||
}
|
||||
|
||||
challengeBytes := []byte(c.Message.String())
|
||||
|
||||
return c.VerifySignedBytes(address, challengeBytes, signature)
|
||||
}
|
||||
|
||||
func (c *Challenge) VerifySignedBytes(expectedAddress string, msg []byte, sig []byte) (bool, error) {
|
||||
msgHash := accounts.TextHash(msg)
|
||||
|
||||
if len(sig) != 65 {
|
||||
return false, fmt.Errorf("invalid signature length: expected 65 bytes, got %d", len(sig))
|
||||
}
|
||||
|
||||
if sig[crypto.RecoveryIDOffset] == 27 || sig[crypto.RecoveryIDOffset] == 28 {
|
||||
sig[crypto.RecoveryIDOffset] -= 27
|
||||
}
|
||||
|
||||
recovered, err := crypto.SigToPub(msgHash, sig)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to recover public key: %w", err)
|
||||
}
|
||||
recoveredAddr := crypto.PubkeyToAddress(*recovered)
|
||||
|
||||
if !common.IsHexAddress(expectedAddress) {
|
||||
return false, fmt.Errorf("invalid Ethereum address format: %s", expectedAddress)
|
||||
}
|
||||
expectedAddr := common.HexToAddress(expectedAddress)
|
||||
|
||||
return strings.EqualFold(expectedAddr.Hex(), recoveredAddr.Hex()), nil
|
||||
}
|
||||
|
||||
func NewUser(pubKey, name, lastName, phoneNumber, email, nationalID string) (*User, error) {
|
||||
|
||||
_, err := phone.IsValid(phoneNumber)
|
||||
_, err := validate.IsValidPhone(phoneNumber)
|
||||
if err != nil {
|
||||
return nil, ErrPhoneInvalid
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = national.IsValid(nationalID)
|
||||
_, err = validate.IsValidNationalID(nationalID)
|
||||
if err != nil {
|
||||
return nil, ErrNationalIDInvalid
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !common.IsHexAddress(pubKey) {
|
||||
return nil, ErrWalletInvalid
|
||||
_, err = validate.IsValidPublicKey(pubKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &User{
|
||||
|
||||
@@ -11,9 +11,7 @@ type UserSession struct {
|
||||
Base
|
||||
UserID uuid.UUID
|
||||
User *User
|
||||
WalletID uuid.UUID
|
||||
IP string
|
||||
Agent string
|
||||
WalletID string
|
||||
ExpireAt time.Time
|
||||
}
|
||||
|
||||
@@ -28,8 +26,6 @@ func CastSessionToStorage(s domain.UserSession) *UserSession {
|
||||
UserID: s.UserID,
|
||||
User: CastUserToStorage(s.User),
|
||||
WalletID: s.WalletID,
|
||||
IP: s.IPaddress,
|
||||
Agent: s.Agent,
|
||||
ExpireAt: s.ExpiresAt,
|
||||
}
|
||||
}
|
||||
@@ -40,8 +36,6 @@ func CastSessionToDomain(s UserSession) *domain.UserSession {
|
||||
UserID: s.UserID,
|
||||
User: *CastUserToDomain(*s.User),
|
||||
WalletID: s.WalletID,
|
||||
IPaddress: s.IP,
|
||||
Agent: s.Agent,
|
||||
ExpiresAt: s.ExpireAt,
|
||||
CreatedAt: s.CreatedAt,
|
||||
UpdatedAt: s.UpdatedAt,
|
||||
|
||||
@@ -48,3 +48,11 @@ func (r *UserRepository) Update(ctx context.Context, user *domain.User) error {
|
||||
func (r *UserRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
return r.db.WithContext(ctx).Delete(&types.User{}, "id = ?", id).Error
|
||||
}
|
||||
|
||||
func (r *UserRepository) GetByPubKey(ctx context.Context, pubKey string) (*domain.User, error) {
|
||||
var user types.User
|
||||
if err := r.db.WithContext(ctx).First(&user, "pub_key = ?", pubKey).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return types.CastUserToDomain(user), nil
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"backend/config"
|
||||
"backend/internal/domain"
|
||||
"backend/pkg/jwt"
|
||||
"backend/pkg/validate/common"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
jwt2 "github.com/golang-jwt/jwt/v5"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
@@ -15,11 +21,10 @@ type AuthService interface {
|
||||
}
|
||||
|
||||
type authService struct {
|
||||
userRepo *domain.UserRepo
|
||||
sessionRepo *domain.SessionRepo
|
||||
challengeExp uint
|
||||
tokenExp uint
|
||||
refreshTokenExp uint
|
||||
userRepo domain.UserRepo
|
||||
sessionRepo domain.SessionRepo
|
||||
challengeExp uint
|
||||
cfg config.JWT
|
||||
}
|
||||
|
||||
type UserToken struct {
|
||||
@@ -29,24 +34,25 @@ type UserToken struct {
|
||||
}
|
||||
|
||||
func NewAuthService(
|
||||
userRepo *domain.UserRepo,
|
||||
sessionRepo *domain.SessionRepo,
|
||||
userRepo domain.UserRepo,
|
||||
sessionRepo domain.SessionRepo,
|
||||
challengeExp uint,
|
||||
tokenExp uint,
|
||||
refreshTokenExp uint) AuthService {
|
||||
cfg config.JWT,
|
||||
) AuthService {
|
||||
return &authService{
|
||||
userRepo: userRepo,
|
||||
sessionRepo: sessionRepo,
|
||||
challengeExp: challengeExp,
|
||||
tokenExp: tokenExp,
|
||||
refreshTokenExp: refreshTokenExp,
|
||||
userRepo: userRepo,
|
||||
sessionRepo: sessionRepo,
|
||||
challengeExp: challengeExp,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *authService) GenerateChallenge(ctx context.Context, pubKey string) (*domain.Challenge, error) {
|
||||
if !common.IsHexAddress(pubKey) {
|
||||
return nil, domain.ErrWalletInvalid
|
||||
_, err := common.IsValidPublicKey(pubKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
challenge := &domain.Challenge{
|
||||
Message: uuid.New(),
|
||||
TimeStamp: time.Now().UTC(),
|
||||
@@ -56,5 +62,68 @@ func (s *authService) GenerateChallenge(ctx context.Context, pubKey string) (*do
|
||||
}
|
||||
|
||||
func (s *authService) Authenticate(ctx context.Context, pubKey string, signature string, challenge *domain.Challenge, chainID uint) (*UserToken, error) {
|
||||
return nil, nil
|
||||
_, err := common.IsValidPublicKey(pubKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid public key: %w", err)
|
||||
}
|
||||
|
||||
isValid, err := challenge.Verify(pubKey, signature)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("signature verification failed: %w", err)
|
||||
}
|
||||
|
||||
if !isValid {
|
||||
return nil, errors.New("invalid signature")
|
||||
}
|
||||
user, err := s.userRepo.GetByPubKey(ctx, pubKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user == nil {
|
||||
return nil, domain.ErrUserNotFound
|
||||
}
|
||||
user.UpdateLastLogin()
|
||||
if err := s.userRepo.Update(ctx, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
expiresAt := time.Now().Add(time.Duration(s.cfg.TokenExpMinutes) * time.Minute)
|
||||
session := domain.NewSession(user.ID, user.PubKey, expiresAt)
|
||||
|
||||
err = s.sessionRepo.Create(ctx, session)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create session: %w", err)
|
||||
}
|
||||
|
||||
claims := &jwt.UserClaims{
|
||||
UserID: uint(user.ID.ID()),
|
||||
Sections: []string{},
|
||||
}
|
||||
claims.ExpiresAt = jwt2.NewNumericDate(expiresAt)
|
||||
claims.IssuedAt = jwt2.NewNumericDate(time.Now())
|
||||
|
||||
authToken, err := jwt.CreateToken([]byte(s.cfg.TokenSecret), claims)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
refreshExpiresAt := time.Now().Add(time.Duration(s.cfg.RefreshTokenExpMinutes) * time.Minute)
|
||||
refreshClaims := &jwt.UserClaims{
|
||||
UserID: uint(user.ID.ID()),
|
||||
Sections: []string{"refresh"},
|
||||
}
|
||||
|
||||
refreshClaims.ExpiresAt = jwt2.NewNumericDate(refreshExpiresAt)
|
||||
refreshClaims.IssuedAt = jwt2.NewNumericDate(time.Now())
|
||||
|
||||
refreshToken, err := jwt.CreateToken([]byte(s.cfg.TokenSecret), refreshClaims)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create refresh token: %w", err)
|
||||
}
|
||||
|
||||
return &UserToken{
|
||||
AuthorizationToken: authToken,
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresAt: expiresAt.Unix(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user