92633f935e
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 18s
CI / ui (pull_request) Successful in 1m7s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m42s
Record the execution platform (kind vk|telegram|direct + device subtype ios|android|web) on each session, captured at creation and carried gateway->backend as a trusted X-Platform header, so the upcoming store-compliance gate has an unforgeable execution context. - backend.sessions gains nullable platform_kind/platform_subtype columns (migration 00011, CHECK-constrained, jet regenerated); session.Platform captures them at mint, resolve returns them, middleware exposes platform(c). kind is derived from the establish endpoint, never a client field; the account-merge session mint inherits the caller's platform. - gateway derives the platform (VK subtype from the signed vk_platform via vkauth, Telegram/direct best-effort from the client) and injects X-Platform on every authenticated backend call through the request context. - ui submits a best-effort device subtype on the telegram/guest/email login requests (new FBS subtype field); VK is server-derived from the signed params. - an unattributed session is untrusted (view-only); VK/TG self-heal on the next cold-start re-mint, direct/email on re-login. Signal plumbing only, no user-visible change; X-Platform is inert until the gate consumes it.
91 lines
3.5 KiB
Go
91 lines
3.5 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; Platform is the raw signed
|
|
// vk_platform value (e.g. "mobile_iphone", "desktop_web"), from which Subtype derives
|
|
// the trusted device family.
|
|
type Identity struct {
|
|
ExternalID string
|
|
Language string
|
|
Platform string
|
|
}
|
|
|
|
// Subtype maps the signed vk_platform to the trusted device family recorded on the
|
|
// session — ios (iPhone/iPad, the store-frozen case), android, or web (desktop/mobile
|
|
// web and anything unrecognised). Because vk_platform rides inside the verified
|
|
// signature, this subtype is trustworthy, unlike a client-reported one.
|
|
func (i Identity) Subtype() string {
|
|
switch {
|
|
case strings.Contains(i.Platform, "android"):
|
|
return "android"
|
|
case strings.Contains(i.Platform, "iphone"), strings.Contains(i.Platform, "ipad"):
|
|
return "ios"
|
|
default:
|
|
return "web"
|
|
}
|
|
}
|
|
|
|
// 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"), Platform: values.Get("vk_platform")}, nil
|
|
}
|