184 lines
4.5 KiB
Go
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)
|
|
} |