package usecase import ( "backend/config" "backend/internal/domain" "backend/pkg/jwt" "backend/pkg/validate/common" "context" "errors" "fmt" "time" jwt2 "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" ) type AuthService interface { GenerateChallenge(ctx context.Context, pubKey string) (*domain.Challenge, error) Authenticate(ctx context.Context, pubKey string, signature string, chainID uint, ipAddress, userAgent string) (*UserToken, error) } type authService struct { userRepo domain.UserRepo sessionRepo domain.SessionRepo challengeRepo domain.ChallengeRepo challengeExp uint cfg config.JWT } type UserToken struct { AuthorizationToken string RefreshToken string ExpiresAt int64 } func NewAuthService( userRepo domain.UserRepo, sessionRepo domain.SessionRepo, challengeRepo domain.ChallengeRepo, challengeExp uint, cfg config.JWT, ) AuthService { return &authService{ userRepo: userRepo, sessionRepo: sessionRepo, challengeRepo: challengeRepo, challengeExp: challengeExp, cfg: cfg, } } func (s *authService) GenerateChallenge(ctx context.Context, pubKey string) (*domain.Challenge, error) { _, err := common.IsValidPublicKey(pubKey) if err != nil { return nil, err } challenge := &domain.Challenge{ Message: uuid.New().String(), TimeStamp: time.Now().UTC(), ExpiresAt: time.Now().Add(time.Duration(s.challengeExp) * time.Minute), } // Save challenge to Redis err = s.challengeRepo.Create(ctx, pubKey, challenge) if err != nil { return nil, fmt.Errorf("failed to save challenge: %w", err) } return challenge, nil } func (s *authService) Authenticate(ctx context.Context, pubKey string, signature string, chainID uint, ipAddress, userAgent string) (*UserToken, error) { _, err := common.IsValidPublicKey(pubKey) if err != nil { return nil, fmt.Errorf("invalid public key: %w", err) } // Retrieve challenge from Redis challenge, err := s.challengeRepo.GetByPubKey(ctx, pubKey) if err != nil { return nil, fmt.Errorf("failed to retrieve challenge: %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") } // Delete the challenge after successful verification to prevent replay attacks err = s.challengeRepo.Delete(ctx, pubKey) if err != nil { return nil, fmt.Errorf("failed to delete challenge: %w", err) } 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 }