package loginwidget import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "errors" "net/url" "sort" "strconv" "strings" "testing" "time" ) const testToken = "123456:TESTTOKEN" // signWidget builds validly signed Login Widget data for the token and fields, // mirroring Telegram's algorithm (secret = SHA-256(token); HMAC over the sorted // data-check string). func signWidget(token string, fields map[string]string) string { keys := make([]string, 0, len(fields)) for k := range fields { keys = append(keys, k) } sort.Strings(keys) lines := make([]string, 0, len(keys)) for _, k := range keys { lines = append(lines, k+"="+fields[k]) } secret := sha256.Sum256([]byte(token)) mac := hmac.New(sha256.New, secret[:]) mac.Write([]byte(strings.Join(lines, "\n"))) v := url.Values{} for k, val := range fields { v.Set(k, val) } v.Set("hash", hex.EncodeToString(mac.Sum(nil))) return v.Encode() } func freshFields() map[string]string { return map[string]string{ "auth_date": strconv.FormatInt(time.Now().Unix(), 10), "id": "42", "username": "neo", "first_name": "Thomas", } } func TestValidateOK(t *testing.T) { data := signWidget(testToken, freshFields()) u, err := NewHMACValidator(testToken).Validate(data) if err != nil { t.Fatalf("validate: %v", err) } if u.ExternalID != "42" || u.Username != "neo" || u.FirstName != "Thomas" { t.Errorf("user = %+v, want {42 neo Thomas}", u) } } func TestValidateRejects(t *testing.T) { valid := signWidget(testToken, freshFields()) t.Run("tampered hash", func(t *testing.T) { tampered := strings.Replace(valid, "hash=", "hash=00", 1) if _, err := NewHMACValidator(testToken).Validate(tampered); !errors.Is(err, ErrInvalidLoginWidget) { t.Errorf("err = %v, want ErrInvalidLoginWidget", err) } }) t.Run("wrong token", func(t *testing.T) { if _, err := NewHMACValidator("other:TOKEN").Validate(valid); !errors.Is(err, ErrInvalidLoginWidget) { t.Errorf("err = %v, want ErrInvalidLoginWidget", err) } }) t.Run("tampered field", func(t *testing.T) { // Flip the id after signing: the HMAC must no longer match. forged := strings.Replace(valid, "id=42", "id=43", 1) if _, err := NewHMACValidator(testToken).Validate(forged); !errors.Is(err, ErrInvalidLoginWidget) { t.Errorf("err = %v, want ErrInvalidLoginWidget", err) } }) t.Run("missing hash", func(t *testing.T) { if _, err := NewHMACValidator(testToken).Validate("id=42&auth_date=1"); !errors.Is(err, ErrInvalidLoginWidget) { t.Errorf("err = %v, want ErrInvalidLoginWidget", err) } }) t.Run("stale auth_date", func(t *testing.T) { stale := signWidget(testToken, map[string]string{ "auth_date": strconv.FormatInt(time.Now().Add(-48*time.Hour).Unix(), 10), "id": "42", }) if _, err := NewHMACValidator(testToken).Validate(stale); !errors.Is(err, ErrInvalidLoginWidget) { t.Errorf("err = %v, want ErrInvalidLoginWidget", err) } }) }