Ctrl K
ComunidadGuías…

internal/domain/telegram_login.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package domain

import (
	"errors"
	"slices"
	"strings"
	"time"
	"unicode/utf8"
)

var (
	ErrTelegramLoginClientInvalid         = errors.New("telegram login client invalid")
	ErrTelegramLoginClientDisabled        = errors.New("telegram login client disabled")
	ErrTelegramLoginURLInvalid            = errors.New("telegram login url invalid")
	ErrTelegramLoginRequestInvalid        = errors.New("telegram login request invalid")
	ErrTelegramLoginRequestExpired        = errors.New("telegram login request expired")
	ErrTelegramLoginRequestConflict       = errors.New("telegram login request conflict")
	ErrTelegramLoginMatchCodeInvalid      = errors.New("telegram login match code invalid")
	ErrTelegramLoginScopeInvalid          = errors.New("telegram login scope invalid")
	ErrTelegramLoginCodeInvalid           = errors.New("telegram login code invalid")
	ErrTelegramLoginCodeConsumed          = errors.New("telegram login code consumed")
	ErrTelegramLoginWebAuthHashInvalid    = errors.New("telegram login web authorization hash invalid")
	ErrTelegramLoginRedirectNotAllowed    = errors.New("telegram login redirect not allowed")
	ErrTelegramLoginOriginNotAllowed      = errors.New("telegram login origin not allowed")
	ErrTelegramLoginSecretInvalid         = errors.New("telegram login client secret invalid")
	ErrTelegramLoginPKCEInvalid           = errors.New("telegram login pkce invalid")
	ErrTelegramLoginAuthorizationsTooMany = errors.New("telegram login authorizations too many")
)

const MaxTelegramLoginWebAuthorizations = 1000

type TelegramLoginSigningAlgorithm string

const (
	TelegramLoginSigningRS256  TelegramLoginSigningAlgorithm = "RS256"
	TelegramLoginSigningES256  TelegramLoginSigningAlgorithm = "ES256"
	TelegramLoginSigningEdDSA  TelegramLoginSigningAlgorithm = "EdDSA"
	TelegramLoginSigningES256K TelegramLoginSigningAlgorithm = "ES256K"
)

func (a TelegramLoginSigningAlgorithm) Valid() bool {
	switch a {
	case TelegramLoginSigningRS256, TelegramLoginSigningES256, TelegramLoginSigningEdDSA, TelegramLoginSigningES256K:
		return true
	default:
		return false
	}
}

type TelegramLoginScope string

const (
	TelegramLoginScopeOpenID    TelegramLoginScope = "openid"
	TelegramLoginScopeProfile   TelegramLoginScope = "profile"
	TelegramLoginScopePhone     TelegramLoginScope = "phone"
	TelegramLoginScopeBotAccess TelegramLoginScope = "telegram:bot_access"
)

func (s TelegramLoginScope) Valid() bool {
	switch s {
	case TelegramLoginScopeOpenID, TelegramLoginScopeProfile, TelegramLoginScopePhone, TelegramLoginScopeBotAccess:
		return true
	default:
		return false
	}
}

type TelegramLoginClient struct {
	BotUserID        int64
	ClientID         string
	SecretHash       []byte
	SecretVersion    int64
	SigningAlgorithm TelegramLoginSigningAlgorithm
	Enabled          bool
	CreatedAt        time.Time
	UpdatedAt        time.Time
}

func (c TelegramLoginClient) Clone() TelegramLoginClient {
	out := c
	out.SecretHash = append([]byte(nil), c.SecretHash...)
	return out
}

func (c TelegramLoginClient) Validate() error {
	if c.BotUserID <= 0 || c.ClientID == "" || len(c.SecretHash) != 32 || c.SecretVersion <= 0 || !c.SigningAlgorithm.Valid() {
		return ErrTelegramLoginClientInvalid
	}
	return nil
}

type TelegramLoginAllowedURLKind string

const (
	TelegramLoginAllowedWebOrigin   TelegramLoginAllowedURLKind = "web_origin"
	TelegramLoginAllowedRedirectURI TelegramLoginAllowedURLKind = "redirect_uri"
)

type TelegramLoginAllowedURL struct {
	ID            int64
	BotUserID     int64
	Kind          TelegramLoginAllowedURLKind
	NormalizedURL string
	CreatedAt     time.Time
}

type TelegramLoginNativePlatform string

const (
	TelegramLoginNativeIOS     TelegramLoginNativePlatform = "ios"
	TelegramLoginNativeAndroid TelegramLoginNativePlatform = "android"
)

type TelegramLoginNativeApp struct {
	ID            int64
	BotUserID     int64
	Platform      TelegramLoginNativePlatform
	ApplicationID string
	// VerificationID is the 10-character Apple Team ID on iOS and the
	// normalized 64-hex SHA-256 signing-certificate fingerprint on Android.
	VerificationID      string
	CallbackURI         string
	VerifiedDisplayName string
	Enabled             bool
	CreatedAt           time.Time
	UpdatedAt           time.Time
}

const MaxTelegramLoginNativeApps = 20

func (p TelegramLoginNativePlatform) Valid() bool {
	return p == TelegramLoginNativeIOS || p == TelegramLoginNativeAndroid
}

func (a TelegramLoginNativeApp) Validate() error {
	if a.BotUserID <= 0 || !a.Platform.Valid() || a.ApplicationID == "" || len(a.ApplicationID) > 255 ||
		a.VerificationID == "" || a.CallbackURI == "" || len(a.CallbackURI) > 4096 ||
		a.VerifiedDisplayName == "" || len(a.VerifiedDisplayName) > 128 ||
		a.CreatedAt.IsZero() || a.UpdatedAt.IsZero() {
		return ErrTelegramLoginClientInvalid
	}
	return nil
}

type TelegramLoginRequestSource string

const (
	TelegramLoginRequestWeb           TelegramLoginRequestSource = "web"
	TelegramLoginRequestJavaScript    TelegramLoginRequestSource = "javascript"
	TelegramLoginRequestNative        TelegramLoginRequestSource = "native"
	TelegramLoginRequestMiniApp       TelegramLoginRequestSource = "mini_app"
	TelegramLoginRequestMessageButton TelegramLoginRequestSource = "message_button"
)

type TelegramLoginRequestState string

const (
	TelegramLoginRequestPending  TelegramLoginRequestState = "pending"
	TelegramLoginRequestApproved TelegramLoginRequestState = "approved"
	TelegramLoginRequestDeclined TelegramLoginRequestState = "declined"
	TelegramLoginRequestExpired  TelegramLoginRequestState = "expired"
)

func (s TelegramLoginRequestState) Terminal() bool {
	return s == TelegramLoginRequestApproved || s == TelegramLoginRequestDeclined || s == TelegramLoginRequestExpired
}

func CanTransitionTelegramLoginRequest(from, to TelegramLoginRequestState) bool {
	if from != TelegramLoginRequestPending {
		return false
	}
	return to == TelegramLoginRequestApproved || to == TelegramLoginRequestDeclined || to == TelegramLoginRequestExpired
}

type TelegramLoginRequest struct {
	ID                  int64
	RequestTokenHash    []byte
	BrowserTokenHash    []byte
	BotUserID           int64
	ClientID            string
	SigningAlgorithm    TelegramLoginSigningAlgorithm
	Source              TelegramLoginRequestSource
	ResponseType        string
	RedirectURI         string
	Origin              string
	Domain              string
	Scopes              []TelegramLoginScope
	State               string
	Nonce               string
	CodeChallenge       string
	CodeChallengeMethod string
	Browser             string
	Platform            string
	IP                  string
	Region              string
	InAppOrigin         string
	IsApp               bool
	VerifiedAppName     string
	MatchCodes          []string
	MatchCode           string
	MatchCodesFirst     bool
	UserIDHint          int64
	PeerType            PeerType
	PeerID              int64
	MessageID           int
	ButtonID            int
	Status              TelegramLoginRequestState
	AuthorizedUserID    int64
	ProfileName         string
	GivenName           string
	FamilyName          string
	PreferredUsername   string
	Picture             string
	PhoneNumber         string
	WriteAllowed        bool
	PhoneShared         bool
	CreatedAt           time.Time
	ExpiresAt           time.Time
	ApprovedAt          time.Time
	DeclinedAt          time.Time
}

func (r TelegramLoginRequest) Clone() TelegramLoginRequest {
	out := r
	out.RequestTokenHash = append([]byte(nil), r.RequestTokenHash...)
	out.BrowserTokenHash = append([]byte(nil), r.BrowserTokenHash...)
	out.Scopes = append([]TelegramLoginScope(nil), r.Scopes...)
	out.MatchCodes = append([]string(nil), r.MatchCodes...)
	return out
}

func (r TelegramLoginRequest) Requests(scope TelegramLoginScope) bool {
	return slices.Contains(r.Scopes, scope)
}

func (r TelegramLoginRequest) Validate() error {
	if len(r.RequestTokenHash) != 32 || len(r.BrowserTokenHash) != 32 || r.BotUserID <= 0 || r.ClientID == "" || r.ClientID != strings.TrimSpace(r.ClientID) || r.RedirectURI == "" || r.Domain == "" {
		return ErrTelegramLoginRequestInvalid
	}
	if !r.SigningAlgorithm.Valid() || !r.Source.Valid() || (r.ResponseType != "code" && r.ResponseType != "post_message" && r.ResponseType != "legacy_url") ||
		r.Status != TelegramLoginRequestPending || r.CreatedAt.IsZero() || !r.ExpiresAt.After(r.CreatedAt) {
		return ErrTelegramLoginRequestInvalid
	}
	switch r.Source {
	case TelegramLoginRequestWeb:
		if r.ResponseType != "code" {
			return ErrTelegramLoginRequestInvalid
		}
	case TelegramLoginRequestJavaScript:
		if r.ResponseType != "post_message" {
			return ErrTelegramLoginRequestInvalid
		}
	case TelegramLoginRequestNative:
		if r.ResponseType != "code" || !r.IsApp || r.VerifiedAppName == "" || r.Origin != "" {
			return ErrTelegramLoginRequestInvalid
		}
	case TelegramLoginRequestMiniApp:
		if r.ResponseType != "post_message" {
			return ErrTelegramLoginRequestInvalid
		}
	case TelegramLoginRequestMessageButton:
		if r.ResponseType != "legacy_url" {
			return ErrTelegramLoginRequestInvalid
		}
	default:
		return ErrTelegramLoginRequestInvalid
	}
	if r.Source != TelegramLoginRequestNative && (r.IsApp || r.VerifiedAppName != "" || r.Origin == "") {
		return ErrTelegramLoginRequestInvalid
	}
	if r.AuthorizedUserID != 0 || r.ProfileName != "" || r.GivenName != "" || r.FamilyName != "" ||
		r.PreferredUsername != "" || r.Picture != "" || r.PhoneNumber != "" || r.WriteAllowed || r.PhoneShared ||
		!r.ApprovedAt.IsZero() || !r.DeclinedAt.IsZero() {
		return ErrTelegramLoginRequestInvalid
	}
	if len(r.RedirectURI) > 4096 || len(r.Origin) > 4096 || len(r.Domain) > 255 || len(r.InAppOrigin) > 4096 ||
		len(r.State) > 2048 || len(r.Nonce) > 1024 || len(r.Browser) == 0 || len(r.Browser) > 255 ||
		len(r.Platform) == 0 || len(r.Platform) > 255 || len(r.IP) == 0 || len(r.IP) > 128 ||
		len(r.Region) == 0 || len(r.Region) > 255 || len(r.VerifiedAppName) > 128 || r.UserIDHint < 0 ||
		r.PeerID < 0 || r.MessageID < 0 || r.ButtonID < 0 || len(r.MatchCodes) > 8 {
		return ErrTelegramLoginRequestInvalid
	}
	if r.ResponseType == "legacy_url" {
		if r.Source != TelegramLoginRequestMessageButton || r.PeerID <= 0 || r.MessageID <= 0 ||
			(r.PeerType != PeerTypeUser && r.PeerType != PeerTypeChannel) || r.CodeChallenge != "" || r.CodeChallengeMethod != "" ||
			len(r.MatchCodes) != 0 || r.MatchCode != "" || r.MatchCodesFirst {
			return ErrTelegramLoginRequestInvalid
		}
		if !slices.Contains(r.Scopes, TelegramLoginScopeOpenID) || !slices.Contains(r.Scopes, TelegramLoginScopeProfile) {
			return ErrTelegramLoginScopeInvalid
		}
		seen := make(map[TelegramLoginScope]struct{}, len(r.Scopes))
		for _, scope := range r.Scopes {
			if !scope.Valid() || scope == TelegramLoginScopePhone {
				return ErrTelegramLoginScopeInvalid
			}
			if _, duplicate := seen[scope]; duplicate {
				return ErrTelegramLoginScopeInvalid
			}
			seen[scope] = struct{}{}
		}
	} else if r.ResponseType == "code" {
		if err := ValidateTelegramLoginScopes(r.Scopes, r.SigningAlgorithm); err != nil {
			return err
		}
		if r.CodeChallengeMethod != "S256" || r.CodeChallenge == "" {
			return ErrTelegramLoginPKCEInvalid
		}
	} else {
		if err := ValidateTelegramLoginScopes(r.Scopes, r.SigningAlgorithm); err != nil {
			return err
		}
		// Telegram's official JavaScript SDK returns an ID token directly and
		// therefore sends no authorization-code PKCE parameters. Accept a PKCE
		// pair for generic callers, but never a partial pair.
		if r.CodeChallenge == "" && r.CodeChallengeMethod == "" {
			// Official post_message/Mini App shape.
		} else if r.CodeChallengeMethod != "S256" || r.CodeChallenge == "" {
			return ErrTelegramLoginPKCEInvalid
		}
	}
	if r.Source == TelegramLoginRequestMiniApp {
		if r.ResponseType != "post_message" || r.InAppOrigin == "" || r.Origin != r.InAppOrigin {
			return ErrTelegramLoginRequestInvalid
		}
	} else if r.InAppOrigin != "" {
		return ErrTelegramLoginRequestInvalid
	}
	if r.MatchCodesFirst && len(r.MatchCodes) == 0 {
		return ErrTelegramLoginRequestInvalid
	}
	if len(r.MatchCodes) > 0 && (r.MatchCode == "" || !slices.Contains(r.MatchCodes, r.MatchCode)) {
		return ErrTelegramLoginRequestInvalid
	}
	return nil
}

// TelegramLoginMessageButtonAuthorization is the domain-only input for the
// legacy login_url consent path. BotToken is used transiently to produce the
// official HMAC response and is never persisted in the login aggregate.
type TelegramLoginMessageButtonAuthorization struct {
	UserID             int64
	BotUserID          int64
	BotToken           string
	URL                string
	RequestWriteAccess bool
	WriteAllowed       bool
	Peer               Peer
	MessageID          int
	ButtonID           int
	Browser            string
	Platform           string
	IP                 string
	Region             string
	Identity           TelegramLoginIdentitySnapshot
}

type TelegramLoginMessageButtonResult struct {
	URL              string
	Request          TelegramLoginRequest
	WebAuthorization TelegramLoginWebAuthorization
}

func (s TelegramLoginRequestSource) Valid() bool {
	switch s {
	case TelegramLoginRequestWeb, TelegramLoginRequestJavaScript, TelegramLoginRequestNative,
		TelegramLoginRequestMiniApp, TelegramLoginRequestMessageButton:
		return true
	default:
		return false
	}
}

// TelegramLoginIdentitySnapshot is the immutable identity presented on the
// approval screen and later signed into the ID token. It is written together
// with the pending->approved transition so a profile/phone mutation between
// approval and code exchange cannot change what the relying party receives.
type TelegramLoginIdentitySnapshot struct {
	UserID            int64
	Name              string
	GivenName         string
	FamilyName        string
	PreferredUsername string
	Picture           string
	PhoneNumber       string
}

func (s TelegramLoginIdentitySnapshot) Sanitized(includeProfile, includePhone bool) (TelegramLoginIdentitySnapshot, error) {
	if s.UserID <= 0 {
		return TelegramLoginIdentitySnapshot{}, ErrTelegramLoginRequestInvalid
	}
	out := TelegramLoginIdentitySnapshot{UserID: s.UserID}
	if includeProfile {
		out.Name = strings.TrimSpace(s.Name)
		out.GivenName = strings.TrimSpace(s.GivenName)
		out.FamilyName = strings.TrimSpace(s.FamilyName)
		out.PreferredUsername = strings.TrimSpace(s.PreferredUsername)
		out.Picture = strings.TrimSpace(s.Picture)
		if out.Name == "" || out.GivenName == "" {
			return TelegramLoginIdentitySnapshot{}, ErrTelegramLoginRequestInvalid
		}
	}
	if includePhone {
		out.PhoneNumber = NormalizePhone(s.PhoneNumber)
		if !ValidPhone(out.PhoneNumber) {
			return TelegramLoginIdentitySnapshot{}, ErrPhoneNumberInvalid
		}
	}
	if !boundedUTF8(out.Name, 255) || !boundedUTF8(out.GivenName, 255) || !boundedUTF8(out.FamilyName, 255) ||
		!boundedUTF8(out.PreferredUsername, 64) || !boundedUTF8(out.Picture, 4096) || len(out.PhoneNumber) > 32 {
		return TelegramLoginIdentitySnapshot{}, ErrTelegramLoginRequestInvalid
	}
	return out, nil
}

func boundedUTF8(value string, maxBytes int) bool {
	return utf8.ValidString(value) && len(value) <= maxBytes
}

func ValidateTelegramLoginScopes(scopes []TelegramLoginScope, alg TelegramLoginSigningAlgorithm) error {
	if !alg.Valid() || len(scopes) == 0 || !slices.Contains(scopes, TelegramLoginScopeOpenID) {
		return ErrTelegramLoginScopeInvalid
	}
	seen := make(map[TelegramLoginScope]struct{}, len(scopes))
	for _, scope := range scopes {
		if !scope.Valid() {
			return ErrTelegramLoginScopeInvalid
		}
		if _, duplicate := seen[scope]; duplicate {
			return ErrTelegramLoginScopeInvalid
		}
		seen[scope] = struct{}{}
	}
	if alg == TelegramLoginSigningEdDSA || alg == TelegramLoginSigningES256K {
		if len(scopes) != 1 || scopes[0] != TelegramLoginScopeOpenID {
			return ErrTelegramLoginScopeInvalid
		}
	}
	return nil
}

type TelegramLoginApproval struct {
	RequestID    int64
	Identity     TelegramLoginIdentitySnapshot
	WriteAllowed bool
	PhoneShared  bool
	MatchCode    string
	ApprovedAt   time.Time
}

type TelegramLoginAuthorizationCode struct {
	ID         int64
	RequestID  int64
	CodeHash   []byte
	SealedCode []byte
	SealNonce  []byte
	SealKeyID  string
	IssuedAt   time.Time
	ExpiresAt  time.Time
	ConsumedAt time.Time
}

// TelegramLoginCodeExchange carries the values already normalized/hashed by
// the application service. The durable store compares them again while the
// code/request/client rows are locked, closing redirect, PKCE and secret-
// rotation TOCTOU gaps between HTTP validation and one-time consumption.
type TelegramLoginCodeExchange struct {
	CodeHash            []byte
	ClientID            string
	ClientSecretVersion int64
	RedirectURI         string
	CodeChallenge       string
	Now                 time.Time
}

func (c TelegramLoginAuthorizationCode) Clone() TelegramLoginAuthorizationCode {
	out := c
	out.CodeHash = append([]byte(nil), c.CodeHash...)
	out.SealedCode = append([]byte(nil), c.SealedCode...)
	out.SealNonce = append([]byte(nil), c.SealNonce...)
	return out
}

type TelegramLoginWebAuthorization struct {
	Hash             int64
	RequestID        int64
	UserID           int64
	BotUserID        int64
	Domain           string
	Browser          string
	Platform         string
	IP               string
	Region           string
	Scopes           []TelegramLoginScope
	PhoneShared      bool
	BotAccessGranted bool
	CreatedAt        time.Time
	LastActiveAt     time.Time
	RevokedAt        time.Time
}

func (a TelegramLoginWebAuthorization) Clone() TelegramLoginWebAuthorization {
	out := a
	out.Scopes = append([]TelegramLoginScope(nil), a.Scopes...)
	return out
}