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) }