Stage 6: gateway edge (Connect/FlatBuffers over h2c, platform/email/guest auth, sessions, rate-limit, admin passthrough, live push bridge)
New public ingress and the first network edge. Framework + a vertical slice of operations end-to-end; remaining ops reuse the same transcode pattern in Stage 7. Contracts (new module scrabble/pkg): - push.proto (backend->gateway gRPC server-stream) + scrabble.fbs (FlatBuffers edge payloads), committed generated Go; buf/flatc Makefiles (dev-time codegen). Backend: - REST handlers on the /api/v1 groups: internal session endpoints (telegram/guest/email login -> mint, resolve, revoke) and the user slice (profile, submit_play, state, lobby enqueue/poll, chat). - internal/notify in-process Publisher hub + internal/pushgrpc gRPC server (BACKEND_GRPC_ADDR) streaming your_turn/opponent_moved/chat/nudge/match_found; emission in game.commit, social, matchmaker. - migration 00005 accounts.is_guest; guests are durable rows excluded from stats; ProvisionGuest; email-as-login (RequestLoginCode/LoginWithCode). Gateway (new module scrabble/gateway): - Connect Gateway service over h2c (Execute + Subscribe), FlatBuffers<->JSON transcode registry, Telegram initData HMAC validator (seam), session cache, token-bucket rate limiter (3 classes), push fan-out hub, backend REST + push gRPC client, admin Basic-Auth reverse proxy. go.work: use ./pkg, ./gateway + replace scrabble/pkg. CI: gateway/**, pkg/** path filters; unit build/vet/test span all three modules. Docs (PLAN, ARCHITECTURE, FUNCTIONAL+ru, TESTING, READMEs) updated; gateway/pkg unit tests + guest/email-login integration tests.
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
// Package auth holds the gateway's credential validators. The only non-trivial
|
||||
// one is the Telegram Web App initData HMAC check; guest and email logins carry
|
||||
// no gateway-side secret and are validated by the backend. The validator is an
|
||||
// interface so handlers test against fixtures without a bot token.
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ErrInvalidInitData is returned when initData fails HMAC validation, is missing
|
||||
// the hash, is malformed, or is older than the freshness window.
|
||||
var ErrInvalidInitData = errors.New("auth: invalid telegram init data")
|
||||
|
||||
// defaultMaxAge bounds how old a validated initData payload may be.
|
||||
const defaultMaxAge = 24 * time.Hour
|
||||
|
||||
// TelegramUser is the identity extracted from a validated initData payload. ID
|
||||
// is the platform user id used as the identity's external_id.
|
||||
type TelegramUser struct {
|
||||
ID string
|
||||
Username string
|
||||
FirstName string
|
||||
}
|
||||
|
||||
// TelegramValidator validates Telegram Web App launch data and returns the
|
||||
// authenticated user.
|
||||
type TelegramValidator interface {
|
||||
Validate(initData string) (TelegramUser, error)
|
||||
}
|
||||
|
||||
// HMACValidator validates initData against a bot token per Telegram's documented
|
||||
// algorithm: the data-check string is HMAC-SHA256'd under a secret derived from
|
||||
// the bot token, and the result is compared with the supplied hash.
|
||||
type HMACValidator struct {
|
||||
botToken string
|
||||
maxAge time.Duration
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
// NewHMACValidator constructs a validator for botToken.
|
||||
func NewHMACValidator(botToken string) *HMACValidator {
|
||||
return &HMACValidator{botToken: botToken, maxAge: defaultMaxAge, now: time.Now}
|
||||
}
|
||||
|
||||
// Validate parses and verifies initData, returning the authenticated user.
|
||||
func (v *HMACValidator) Validate(initData string) (TelegramUser, error) {
|
||||
values, err := url.ParseQuery(initData)
|
||||
if err != nil {
|
||||
return TelegramUser{}, ErrInvalidInitData
|
||||
}
|
||||
hash := values.Get("hash")
|
||||
if hash == "" {
|
||||
return TelegramUser{}, ErrInvalidInitData
|
||||
}
|
||||
values.Del("hash")
|
||||
|
||||
if !v.checkSignature(values, hash) {
|
||||
return TelegramUser{}, ErrInvalidInitData
|
||||
}
|
||||
if err := v.checkFreshness(values.Get("auth_date")); err != nil {
|
||||
return TelegramUser{}, err
|
||||
}
|
||||
return parseUser(values.Get("user"))
|
||||
}
|
||||
|
||||
// checkSignature recomputes the HMAC over the sorted data-check string and
|
||||
// compares it with hash in constant time.
|
||||
func (v *HMACValidator) checkSignature(values url.Values, hash string) bool {
|
||||
keys := make([]string, 0, len(values))
|
||||
for k := range values {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
lines := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
lines = append(lines, k+"="+values.Get(k))
|
||||
}
|
||||
dataCheck := strings.Join(lines, "\n")
|
||||
|
||||
secret := hmacSHA256([]byte("WebAppData"), []byte(v.botToken))
|
||||
want := hmacSHA256(secret, []byte(dataCheck))
|
||||
got, err := hex.DecodeString(hash)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return hmac.Equal(want, got)
|
||||
}
|
||||
|
||||
// checkFreshness rejects an auth_date older than the validator's window.
|
||||
func (v *HMACValidator) checkFreshness(authDate string) error {
|
||||
if authDate == "" {
|
||||
return ErrInvalidInitData
|
||||
}
|
||||
secs, err := strconv.ParseInt(authDate, 10, 64)
|
||||
if err != nil {
|
||||
return ErrInvalidInitData
|
||||
}
|
||||
if v.now().Sub(time.Unix(secs, 0)) > v.maxAge {
|
||||
return ErrInvalidInitData
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseUser extracts the user id and names from the user JSON field.
|
||||
func parseUser(userJSON string) (TelegramUser, error) {
|
||||
if userJSON == "" {
|
||||
return TelegramUser{}, ErrInvalidInitData
|
||||
}
|
||||
var u struct {
|
||||
ID int64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
FirstName string `json:"first_name"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(userJSON), &u); err != nil || u.ID == 0 {
|
||||
return TelegramUser{}, ErrInvalidInitData
|
||||
}
|
||||
return TelegramUser{
|
||||
ID: strconv.FormatInt(u.ID, 10),
|
||||
Username: u.Username,
|
||||
FirstName: u.FirstName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// hmacSHA256 returns HMAC-SHA256(message) under key.
|
||||
func hmacSHA256(key, message []byte) []byte {
|
||||
h := hmac.New(sha256.New, key)
|
||||
h.Write(message)
|
||||
return h.Sum(nil)
|
||||
}
|
||||
Reference in New Issue
Block a user