65c194264c
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 1m0s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m19s
Mirror the Telegram Mini App wrapper for VK: the SPA loads at a new /vk/ entry, authenticates from VK's signed launch parameters, and provisions a 'vk' platform identity — the minimum to run the game in VK test mode. - Gateway verifies the launch signature in-process (internal/vkauth: HMAC-SHA256 over the sorted vk_* params under GATEWAY_VK_APP_SECRET, base64url) — a pure offline check, no side-service. New auth.vk op (gated on the secret), backendclient.VKAuth, /vk/ SPA mount. - Backend: KindVK + ProvisionVK/vkSeed, /sessions/vk handler, identity kind widened to include 'vk' (migration 00005, expand-contract). - UI: src/lib/vk.ts (VK Bridge, lazy-imported), bootVK + the /vk/ boot dispatch, encodeVKLogin + authVK across transport/client/mock. VK omits the name from the signed params, so the client reads it via VKWebAppGetUserInfo as an unsigned display seed. - Deploy: /vk in the edge Caddyfile, GATEWAY_VK_APP_SECRET wired through compose + .env.example + CI (TEST_) + prod-deploy (PROD_). - Admin console: surface the VK user id (link to the VK profile) next to the Telegram id on the user card. - Docs: ARCHITECTURE §12/§13, FUNCTIONAL (+ _ru), gateway README; VK integration reference under .claude/. Signature algorithm verified against dev.vk.com plus independent Node/Python references and a %2C edge-case vector.
73 lines
2.7 KiB
Go
73 lines
2.7 KiB
Go
// Package vkauth verifies VK Mini App launch parameters in-process. VK signs the
|
|
// launch query string with the app's protected ("secure") key; the gateway holds that
|
|
// secret (GATEWAY_VK_APP_SECRET) and validates the `sign` itself rather than calling a
|
|
// side-service, because the check is a pure offline HMAC with no VK API round-trip
|
|
// (unlike the Telegram validator, which lives in a separate process to isolate the bot
|
|
// token). See docs/ARCHITECTURE.md §12.
|
|
package vkauth
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"crypto/subtle"
|
|
"encoding/base64"
|
|
"errors"
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
// ErrInvalid is returned when launch params fail signature verification, are
|
|
// malformed, carry no signed vk_* parameters, or lack the vk_user_id.
|
|
var ErrInvalid = errors.New("vkauth: invalid vk launch params")
|
|
|
|
// Identity is the user extracted from verified VK launch params. ExternalID is the
|
|
// vk_user_id used as the identities external_id; Language is the vk_language hint that
|
|
// seeds a brand-new account's preferred language.
|
|
type Identity struct {
|
|
ExternalID string
|
|
Language string
|
|
}
|
|
|
|
// Verify checks the `sign` of a VK Mini App launch query string against secret and
|
|
// returns the launching user's identity. Per VK's documented algorithm the signature
|
|
// is HMAC-SHA256 over the vk_*-prefixed parameters — sorted by key and serialized as a
|
|
// URL-encoded query string (url.Values.Encode mirrors VK's reference serialization for
|
|
// the constrained launch-parameter charset) — under the app secret, then base64url
|
|
// without padding; the comparison is constant-time.
|
|
//
|
|
// VK launch params carry no built-in expiry (unlike Telegram's auth_date), so freshness
|
|
// is deliberately not enforced here: the gateway mints its own short-lived session, and
|
|
// a replay only re-authenticates the same vk_user_id.
|
|
func Verify(params, secret string) (Identity, error) {
|
|
values, err := url.ParseQuery(params)
|
|
if err != nil {
|
|
return Identity{}, ErrInvalid
|
|
}
|
|
sign := values.Get("sign")
|
|
if sign == "" {
|
|
return Identity{}, ErrInvalid
|
|
}
|
|
// Only vk_*-prefixed parameters are signed; any other query parameter the client
|
|
// appended is outside the signature and must be excluded from the check.
|
|
signed := url.Values{}
|
|
for k, v := range values {
|
|
if strings.HasPrefix(k, "vk_") {
|
|
signed[k] = v
|
|
}
|
|
}
|
|
if len(signed) == 0 {
|
|
return Identity{}, ErrInvalid
|
|
}
|
|
mac := hmac.New(sha256.New, []byte(secret))
|
|
mac.Write([]byte(signed.Encode()))
|
|
want := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
|
|
if subtle.ConstantTimeCompare([]byte(want), []byte(sign)) != 1 {
|
|
return Identity{}, ErrInvalid
|
|
}
|
|
externalID := values.Get("vk_user_id")
|
|
if externalID == "" {
|
|
return Identity{}, ErrInvalid
|
|
}
|
|
return Identity{ExternalID: externalID, Language: values.Get("vk_language")}, nil
|
|
}
|