feat: add EIP712 verification, auth service implemention and jwt gen

This commit is contained in:
2025-09-07 14:35:40 +03:30
parent 6d16a6ea8c
commit 5c5df48644
10 changed files with 244 additions and 62 deletions
+3 -6
View File
@@ -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
View File
@@ -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{
+1 -7
View File
@@ -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,
+8
View File
@@ -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
}
+87 -18
View File
@@ -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
}