Files
SERTY ea1e5bbf6a 0302-добавил куки и сломал десктоп авторизацию.
сложно поддерживать однояйцевых близнецов - desktop и TMA, подготовил к рефакторингу структуры
2026-02-03 09:32:02 +03:00

225 lines
7.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package middleware
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"rmser/internal/domain/account"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
)
// Claims представляет JWT claims для токена авторизации
type Claims struct {
UserID uuid.UUID `json:"user_id"`
TelegramID int64 `json:"telegram_id"`
jwt.RegisteredClaims
}
// AuthMiddleware проверяет JWT токен (Desktop) или initData от Telegram
func AuthMiddleware(accountRepo account.Repository, authService interface {
ValidateAndExtendSession(token string) (*account.User, error)
}, botToken string, secretKey string, maintenanceMode bool, devIDs []int64) gin.HandlerFunc {
return func(c *gin.Context) {
// 1. Пробуем извлечь токен из Authorization header
tokenString := extractToken(c)
// 2. Если нет токена в header, проверяем куку rmser_session
if tokenString == "" {
cookie, err := c.Cookie("rmser_session")
if err == nil && cookie != "" {
tokenString = cookie
}
}
if tokenString == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Авторизация отклонена: отсутствует токен или подпись"})
return
}
// 3. Попытка 1: Проверяем JWT токен (Desktop)
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("неожиданный метод подписи: %v", token.Header["alg"])
}
return []byte(secretKey), nil
})
if err == nil && token.Valid {
// JWT токен валиден
if claims, ok := token.Claims.(*Claims); ok {
// Проверка режима обслуживания: если включен, разрешаем доступ только разработчикам
if maintenanceMode {
isDev := false
for _, devID := range devIDs {
if claims.TelegramID == devID {
isDev = true
break
}
}
if !isDev {
c.AbortWithStatusJSON(503, gin.H{"error": "maintenance_mode", "message": "Сервис на обслуживании"})
return
}
}
c.Set("userID", claims.UserID)
c.Set("telegramID", claims.TelegramID)
c.Next()
return
}
}
// 4. Попытка 2: Проверяем сессию через authService (HttpOnly Cookie)
// Важно: проверяем длину, чтобы не слать initData (длинную строку) в БД как токен сессии
if authService != nil && len(tokenString) <= 128 {
user, err := authService.ValidateAndExtendSession(tokenString)
if err == nil && user != nil {
// Сессия валидна и продлена
if maintenanceMode {
isDev := false
for _, devID := range devIDs {
if user.TelegramID == devID {
isDev = true
break
}
}
if !isDev {
c.AbortWithStatusJSON(503, gin.H{"error": "maintenance_mode", "message": "Сервис на обслуживании"})
return
}
}
c.Set("userID", user.ID)
c.Set("telegramID", user.TelegramID)
c.Next()
return
}
}
// 5. Попытка 3: Проверяем Telegram InitData
isValid, err := verifyTelegramInitData(tokenString, botToken)
if !isValid || err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Критическая ошибка безопасности: недействительный токен или подпись"})
return
}
// Извлекаем User ID из проверенных данных
values, _ := url.ParseQuery(tokenString)
userJSON := values.Get("user")
// Извлекаем id вручную из JSON-подобной строки или через простой парсинг
// Telegram передает user как JSON-объект: {"id":12345,"first_name":"..."}
tgID, err := extractIDFromUserJSON(userJSON)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Не удалось извлечь Telegram ID"})
return
}
// Проверка режима обслуживания: если включен, разрешаем доступ только разработчикам
if maintenanceMode {
isDev := false
for _, devID := range devIDs {
if tgID == devID {
isDev = true
break
}
}
if !isDev {
c.AbortWithStatusJSON(503, gin.H{"error": "maintenance_mode", "message": "Сервис на обслуживании"})
return
}
}
// Ищем пользователя в БД
user, err := accountRepo.GetUserByTelegramID(tgID)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Пользователь не зарегистрирован. Начните диалог с ботом."})
return
}
c.Set("userID", user.ID)
c.Set("telegramID", tgID)
c.Next()
}
}
// extractToken извлекает токен из Authorization header
func extractToken(c *gin.Context) string {
authHeader := c.GetHeader("Authorization")
if strings.HasPrefix(authHeader, "Bearer ") {
return strings.TrimPrefix(authHeader, "Bearer ")
}
// Оставляем лазейку для отладки ТОЛЬКО если это не production режим
return c.Query("_auth")
}
// verifyTelegramInitData реализует алгоритм проверки Telegram
func verifyTelegramInitData(initData, token string) (bool, error) {
values, err := url.ParseQuery(initData)
if err != nil {
return false, err
}
hash := values.Get("hash")
if hash == "" {
return false, fmt.Errorf("no hash found")
}
values.Del("hash")
// Сортируем ключи
keys := make([]string, 0, len(values))
for k := range values {
keys = append(keys, k)
}
sort.Strings(keys)
// Собираем data_check_string
var dataCheckArr []string
for _, k := range keys {
dataCheckArr = append(dataCheckArr, fmt.Sprintf("%s=%s", k, values.Get(k)))
}
dataCheckString := strings.Join(dataCheckArr, "\n")
// Вычисляем секретный ключ: HMAC-SHA256("WebAppData", token)
sha := sha256.New()
sha.Write([]byte(token))
secretKey := hmac.New(sha256.New, []byte("WebAppData"))
secretKey.Write([]byte(token))
// Вычисляем финальный HMAC
h := hmac.New(sha256.New, secretKey.Sum(nil))
h.Write([]byte(dataCheckString))
expectedHash := hex.EncodeToString(h.Sum(nil))
return expectedHash == hash, nil
}
// Упрощенное извлечение ID из JSON-строки поля user
func extractIDFromUserJSON(userJSON string) (int64, error) {
// Ищем "id":(\d+)
// Для надежности в будущем можно использовать json.Unmarshal
startIdx := strings.Index(userJSON, "\"id\":")
if startIdx == -1 {
return 0, fmt.Errorf("id not found")
}
startIdx += 5
endIdx := strings.IndexAny(userJSON[startIdx:], ",}")
if endIdx == -1 {
return 0, fmt.Errorf("invalid json")
}
idStr := userJSON[startIdx : startIdx+endIdx]
return strconv.ParseInt(idStr, 10, 64)
}