buhapp-backend/internal/telegram/auth.go
ga c6d219c51f Telegram WebApp support + consent endpoint
- Telegram bot (long polling)
- /api/v1/auth/telegram (validate initData, auto-create user)
- /api/v1/auth/consents (record acceptances)
- users.telegram_id column, GetByTelegramID/CreateWithTelegramID
- ValidateInitData (HMAC-SHA256 with WebAppData secret)
2026-08-20 17:43:20 +00:00

155 lines
4.0 KiB
Go

package telegram
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"sort"
"strconv"
"strings"
"time"
"github.com/google/uuid"
)
// WebAppUser — пользователь из Telegram WebApp
type WebAppUser struct {
ID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name,omitempty"`
Username string `json:"username,omitempty"`
LanguageCode string `json:"language_code,omitempty"`
IsBot bool `json:"is_bot,omitempty"`
IsPremium bool `json:"is_premium,omitempty"`
PhotoURL string `json:"photo_url,omitempty"`
}
// InitData — сырой initData от Telegram WebApp
type InitData struct {
QueryID string `json:"query_id,omitempty"`
User *WebAppUser `json:"user,omitempty"`
Receiver *WebAppUser `json:"receiver,omitempty"`
Chat *Chat `json:"chat,omitempty"`
ChatType string `json:"chat_type,omitempty"`
ChatInst string `json:"chat_instance,omitempty"`
StartParam string `json:"start_param,omitempty"`
CanSendAfter int `json:"can_send_after,omitempty"`
AuthDate int64 `json:"auth_date"`
Hash string `json:"hash"`
Signature string `json:"signature,omitempty"`
}
type Chat struct {
ID int64 `json:"id"`
Type string `json:"type"`
Title string `json:"title,omitempty"`
Username string `json:"username,omitempty"`
}
// ValidateInitData — проверка подписи Telegram
func ValidateInitData(botToken string, initData string, maxAge time.Duration) (*InitData, error) {
parts := strings.Split(initData, "&")
if len(parts) == 0 {
return nil, errors.New("empty initData")
}
m := make(map[string]string)
for _, p := range parts {
kv := strings.SplitN(p, "=", 2)
if len(kv) != 2 {
continue
}
m[kv[0]] = kv[1]
}
hash, ok := m["hash"]
if !ok {
return nil, errors.New("no hash")
}
delete(m, "hash")
// сортируем ключи
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
// собираем data-check-string
var b strings.Builder
for i, k := range keys {
if i > 0 {
b.WriteByte('\n')
}
b.WriteString(k)
b.WriteByte('=')
b.WriteString(m[k])
}
checkString := b.String()
// secret = HMAC-SHA256(bot_token, "WebAppData")
secret := hmac.New(sha256.New, []byte("WebAppData"))
secret.Write([]byte(botToken))
secretBytes := secret.Sum(nil)
// hash = HMAC-SHA256(check_string, secret)
h := hmac.New(sha256.New, secretBytes)
h.Write([]byte(checkString))
computed := hex.EncodeToString(h.Sum(nil))
if !hmac.Equal([]byte(computed), []byte(hash)) {
return nil, errors.New("invalid signature")
}
// парсим initData
var data InitData
data.Hash = hash
if v, ok := m["auth_date"]; ok {
ts, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return nil, errors.New("bad auth_date")
}
data.AuthDate = ts
// проверка свежести
if maxAge > 0 {
if time.Now().Unix()-ts > int64(maxAge.Seconds()) {
return nil, errors.New("initData expired")
}
}
}
if v, ok := m["query_id"]; ok { data.QueryID = v }
if v, ok := m["chat_type"]; ok { data.ChatType = v }
if v, ok := m["chat_instance"]; ok { data.ChatInst = v }
if v, ok := m["start_param"]; ok { data.StartParam = v }
if v, ok := m["can_send_after"]; ok {
n, _ := strconv.Atoi(v)
data.CanSendAfter = n
}
if v, ok := m["user"]; ok {
var u WebAppUser
if err := json.Unmarshal([]byte(v), &u); err == nil {
data.User = &u
}
}
if v, ok := m["receiver"]; ok {
var u WebAppUser
if err := json.Unmarshal([]byte(v), &u); err == nil {
data.Receiver = &u
}
}
if v, ok := m["chat"]; ok {
var c Chat
if err := json.Unmarshal([]byte(v), &c); err == nil {
data.Chat = &c
}
}
return &data, nil
}
// UserIDFromTelegram — стабильный UUID из tg_id
func UserIDFromTelegram(tgID int64) uuid.UUID {
ns := uuid.MustParse("1d4a3f50-2a8a-4f55-b1a3-b7e0c1f4e6a0")
return uuid.NewSHA1(ns, []byte(strconv.FormatInt(tgID, 10)))
}