Files
rmser/internal/transport/http/middleware/auth.go

187 lines
5.9 KiB
Go
Raw 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, botToken string, secretKey string, maintenanceMode bool, devIDs []int64) gin.HandlerFunc {
return func(c *gin.Context) {
// 1. Извлекаем данные авторизации
authHeader := c.GetHeader("Authorization")
var authData string
if strings.HasPrefix(authHeader, "Bearer ") {
authData = strings.TrimPrefix(authHeader, "Bearer ")
} else {
// Оставляем лазейку для отладки ТОЛЬКО если это не production режим
// В реальности лучше всегда требовать подпись
authData = c.Query("_auth")
}
if authData == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Авторизация отклонена: отсутствует токен или подпись"})
return
}
// 2. Попытка 1: Проверяем JWT токен (Desktop)
token, err := jwt.ParseWithClaims(authData, &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
}
}
// 3. Попытка 2: Проверяем Telegram InitData
isValid, err := verifyTelegramInitData(authData, botToken)
if !isValid || err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Критическая ошибка безопасности: недействительный токен или подпись"})
return
}
// Извлекаем User ID из проверенных данных
values, _ := url.ParseQuery(authData)
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()
}
}
// 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)
}