212 lines
4.6 KiB
Go
212 lines
4.6 KiB
Go
package auth
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"wish-list-api/pkg/entities"
|
|
"wish-list-api/pkg/user"
|
|
)
|
|
|
|
const MaxAuthAge = 86400
|
|
|
|
type TelegramAuthService interface {
|
|
AuthenticateWithTelegram(data *entities.TelegramAuthRequest) (*entities.TokenPair, error)
|
|
ValidateTelegramAuthData(data *entities.TelegramAuthRequest) error
|
|
}
|
|
|
|
type telegramAuthService struct {
|
|
userService user.Service
|
|
authService Service
|
|
botToken string
|
|
telegramAppUrl string
|
|
}
|
|
|
|
type TelegramAuthConfig struct {
|
|
UserService user.Service
|
|
AuthService Service
|
|
BotToken string
|
|
TelegramAppUrl string
|
|
}
|
|
|
|
func NewTelegramAuthService(config TelegramAuthConfig) TelegramAuthService {
|
|
if config.BotToken == "" {
|
|
config.BotToken = os.Getenv("TELEGRAM_BOT_TOKEN")
|
|
}
|
|
|
|
if config.TelegramAppUrl == "" {
|
|
config.TelegramAppUrl = os.Getenv("TELEGRAM_APP_URL")
|
|
}
|
|
|
|
return &telegramAuthService{
|
|
userService: config.UserService,
|
|
authService: config.AuthService,
|
|
botToken: config.BotToken,
|
|
telegramAppUrl: config.TelegramAppUrl,
|
|
}
|
|
}
|
|
|
|
func (s *telegramAuthService) AuthenticateWithTelegram(data *entities.TelegramAuthRequest) (*entities.TokenPair, error) {
|
|
err := s.ValidateTelegramAuthData(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
user, err := s.userService.GetUserByTelegramID(data.TelegramID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if user == nil {
|
|
randomPassword, err := generateRandomPassword(16)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
email := fmt.Sprintf("telegram_%d@%s", data.TelegramID, s.telegramAppUrl)
|
|
|
|
user = &entities.User{
|
|
Email: email,
|
|
Password: randomPassword,
|
|
TelegramID: data.TelegramID,
|
|
TelegramUsername: data.Username,
|
|
FirstName: data.FirstName,
|
|
LastName: data.LastName,
|
|
PhotoURL: data.PhotoURL,
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
LastLoginDate: time.Now(),
|
|
}
|
|
|
|
hashedPassword, err := s.authService.HashPassword(randomPassword)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
user.Password = hashedPassword
|
|
|
|
user, err = s.userService.CreateUser(user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
updated := false
|
|
|
|
if user.TelegramUsername != data.Username {
|
|
user.TelegramUsername = data.Username
|
|
updated = true
|
|
}
|
|
|
|
if user.FirstName != data.FirstName {
|
|
user.FirstName = data.FirstName
|
|
updated = true
|
|
}
|
|
|
|
if user.LastName != data.LastName {
|
|
user.LastName = data.LastName
|
|
updated = true
|
|
}
|
|
|
|
if user.PhotoURL != data.PhotoURL && data.PhotoURL != "" {
|
|
user.PhotoURL = data.PhotoURL
|
|
updated = true
|
|
}
|
|
|
|
user.LastLoginDate = time.Now()
|
|
updated = true
|
|
|
|
if updated {
|
|
_, err = s.userService.UpdateUserTelegramData(user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
loginRequest := &entities.LoginRequest{
|
|
Email: user.Email,
|
|
}
|
|
|
|
return s.authService.Login(loginRequest)
|
|
}
|
|
|
|
func (s *telegramAuthService) ValidateTelegramAuthData(data *entities.TelegramAuthRequest) error {
|
|
now := time.Now().Unix()
|
|
if now-data.AuthDate > MaxAuthAge {
|
|
return errors.New("данные аутентификации Telegram устарели")
|
|
}
|
|
|
|
secretKey := generateSecretKey(s.botToken)
|
|
|
|
dataCheckString := buildDataCheckString(data)
|
|
hash := generateHash(dataCheckString, secretKey)
|
|
|
|
if hash != data.Hash {
|
|
return errors.New("неверная подпись данных аутентификации Telegram")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func generateSecretKey(botToken string) []byte {
|
|
h := sha256.New()
|
|
h.Write([]byte(botToken))
|
|
return h.Sum(nil)
|
|
}
|
|
|
|
func buildDataCheckString(data *entities.TelegramAuthRequest) string {
|
|
params := map[string]string{
|
|
"auth_date": strconv.FormatInt(data.AuthDate, 10),
|
|
"first_name": data.FirstName,
|
|
"telegram_id": strconv.FormatInt(data.TelegramID, 10),
|
|
}
|
|
|
|
if data.LastName != "" {
|
|
params["last_name"] = data.LastName
|
|
}
|
|
|
|
if data.Username != "" {
|
|
params["username"] = data.Username
|
|
}
|
|
|
|
if data.PhotoURL != "" {
|
|
params["photo_url"] = data.PhotoURL
|
|
}
|
|
|
|
var keys []string
|
|
for k := range params {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
var pairs []string
|
|
for _, k := range keys {
|
|
pairs = append(pairs, fmt.Sprintf("%s=%s", k, params[k]))
|
|
}
|
|
|
|
return strings.Join(pairs, "\n")
|
|
}
|
|
|
|
func generateHash(data string, secretKey []byte) string {
|
|
h := hmac.New(sha256.New, secretKey)
|
|
h.Write([]byte(data))
|
|
return hex.EncodeToString(h.Sum(nil))
|
|
}
|
|
|
|
func generateRandomPassword(length int) (string, error) {
|
|
bytes := make([]byte, length)
|
|
_, err := rand.Read(bytes)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return hex.EncodeToString(bytes)[:length], nil
|
|
}
|