153 lines
3.9 KiB
Go
153 lines
3.9 KiB
Go
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
|
|
}
|