sno-quiz/backend/internal/middleware/auth.go
2025-09-17 22:22:14 +03:00

184 lines
4.5 KiB
Go

package middleware
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net/url"
"sort"
"strconv"
"strings"
"time"
"github.com/gofiber/fiber/v2"
)
// TelegramUserData represents the parsed Telegram user data
type TelegramUserData struct {
ID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name,omitempty"`
Username string `json:"username,omitempty"`
PhotoURL string `json:"photo_url,omitempty"`
AuthDate int64 `json:"auth_date"`
Hash string `json:"hash"`
}
// AuthConfig holds configuration for the auth middleware
type AuthConfig struct {
BotToken string
}
// AuthMiddleware creates a middleware for Telegram WebApp authentication
func AuthMiddleware(config AuthConfig) fiber.Handler {
return func(c *fiber.Ctx) error {
// Skip authentication for OPTIONS preflight requests
if c.Method() == "OPTIONS" {
return c.Next()
}
// Get initData from header
initData := c.Get("X-Telegram-WebApp-Init-Data")
if initData == "" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"success": false,
"message": "Missing Telegram init data",
})
}
// Parse and validate the init data
userData, err := ValidateTelegramInitData(initData, config.BotToken)
if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"success": false,
"message": fmt.Sprintf("Invalid Telegram init data: %v", err),
})
}
// Check if auth date is not too old (5 minutes)
authTime := time.Unix(userData.AuthDate, 0)
if time.Since(authTime) > 5*time.Minute {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"success": false,
"message": "Auth data expired",
})
}
// Store user data in context for handlers to use
c.Locals("telegram_user", userData)
return c.Next()
}
}
// ValidateTelegramInitData validates Telegram WebApp init data
func ValidateTelegramInitData(initData, botToken string) (*TelegramUserData, error) {
// Parse query parameters
values, err := url.ParseQuery(initData)
if err != nil {
return nil, fmt.Errorf("failed to parse init data: %w", err)
}
// Extract hash
hash := values.Get("hash")
if hash == "" {
return nil, errors.New("missing hash in init data")
}
// Build data check string
var dataCheckStrings []string
// Collect all parameters except hash
for key, vals := range values {
if key == "hash" {
continue
}
for _, val := range vals {
dataCheckStrings = append(dataCheckStrings, fmt.Sprintf("%s=%s", key, val))
}
}
// Sort parameters alphabetically
sort.Strings(dataCheckStrings)
// Build data check string
dataCheckString := strings.Join(dataCheckStrings, "\n")
// Create secret key
secretKey := hmac.New(sha256.New, []byte("WebAppData"))
secretKey.Write([]byte(botToken))
// Calculate HMAC
h := hmac.New(sha256.New, secretKey.Sum(nil))
h.Write([]byte(dataCheckString))
calculatedHash := hex.EncodeToString(h.Sum(nil))
// Compare hashes
if calculatedHash != hash {
return nil, errors.New("hash verification failed")
}
// Parse user data
userData := &TelegramUserData{
Hash: hash,
AuthDate: mustParseInt64(values.Get("auth_date")),
}
// Parse user field if present
if userStr := values.Get("user"); userStr != "" {
// Parse user JSON data
var userMap map[string]interface{}
if err := json.Unmarshal([]byte(userStr), &userMap); err != nil {
return nil, fmt.Errorf("failed to parse user JSON data: %w", err)
}
// Extract user data from JSON
if id, ok := userMap["id"].(float64); ok {
userData.ID = int64(id)
}
if firstName, ok := userMap["first_name"].(string); ok {
userData.FirstName = firstName
}
if lastName, ok := userMap["last_name"].(string); ok {
userData.LastName = lastName
}
if username, ok := userMap["username"].(string); ok {
userData.Username = username
}
if photoURL, ok := userMap["photo_url"].(string); ok {
userData.PhotoURL = photoURL
}
} else {
}
return userData, nil
}
// mustParseInt64 helper function to parse int64
func mustParseInt64(s string) int64 {
i, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return 0
}
return i
}
// GetTelegramUser helper function to get user data from context
func GetTelegramUser(c *fiber.Ctx) *TelegramUserData {
if user, ok := c.Locals("telegram_user").(*TelegramUserData); ok {
return user
}
return nil
}
// RequireAuth is a convenience function that returns a handler requiring authentication
func RequireAuth(config AuthConfig) fiber.Handler {
return AuthMiddleware(config)
}