Ctrl K
ComunidadGuías…

oauth/telegram.go

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

Archivo original ↗Preguntar a KiBot
package oauth

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/QuantumNous/new-api/common"
	"github.com/QuantumNous/new-api/model"
	"github.com/QuantumNous/new-api/setting/system_setting"
	"github.com/coreos/go-oidc/v3/oidc"
	"github.com/gin-gonic/gin"
	"golang.org/x/oauth2"
)

const (
	TelegramIssuer              = "https://oauth.telegram.org"
	TelegramOAuthFlowContextKey = "telegram_oauth_flow"
)

var (
	ErrTelegramOAuthNotConfigured = errors.New("Telegram OAuth is not configured or enabled. Please contact your administrator.")
	ErrTelegramOAuthConflict      = errors.New("The telegram OAuth provider name is reserved. Ask your administrator to rename the conflicting custom provider.")
	ErrTelegramOAuthFailed        = errors.New("Telegram authorization failed. Please try again.")
	ErrTelegramAccountNotBound    = errors.New("This Telegram account is not linked. Sign in using another method and link it first.")
)

// TelegramOAuthFlow keeps the PKCE secret and original client configuration on
// the server. Only AuthorizationURL is sent to the browser.
type TelegramOAuthFlow struct {
	CodeVerifier string `json:"code_verifier"`
	ClientID     string `json:"client_id"`
	RedirectURI  string `json:"redirect_uri"`
}

func TelegramConfigurationError() error {
	if HasCustomProviderConflict("telegram") {
		return ErrTelegramOAuthConflict
	}
	if !common.TelegramOAuthEnabled || !system_setting.GetTelegramSettings().IsConfigured() {
		return ErrTelegramOAuthNotConfigured
	}
	return nil
}

func NewTelegramOAuthFlow() (*TelegramOAuthFlow, error) {
	if err := TelegramConfigurationError(); err != nil {
		return nil, err
	}
	redirectURI := strings.TrimRight(system_setting.ServerAddress, "/") + "/oauth/telegram"
	parsed, err := url.Parse(redirectURI)
	if err != nil || parsed.Host == "" || (parsed.Scheme != "https" && parsed.Scheme != "http") {
		return nil, ErrTelegramOAuthNotConfigured
	}
	return &TelegramOAuthFlow{
		CodeVerifier: oauth2.GenerateVerifier(),
		ClientID:     strings.TrimSpace(system_setting.GetTelegramSettings().ClientID),
		RedirectURI:  redirectURI,
	}, nil
}

func (flow *TelegramOAuthFlow) AuthorizationURL(state string) string {
	values := url.Values{
		"client_id":             {flow.ClientID},
		"redirect_uri":          {flow.RedirectURI},
		"response_type":         {"code"},
		"scope":                 {"openid profile"},
		"state":                 {state},
		"code_challenge":        {oauth2.S256ChallengeFromVerifier(flow.CodeVerifier)},
		"code_challenge_method": {"S256"},
	}
	return TelegramIssuer + "/auth?" + values.Encode()
}

type TelegramProvider struct {
	client *http.Client
	keys   oidc.KeySet
}

func init() {
	Register("telegram", NewTelegramProvider(&http.Client{Timeout: 20 * time.Second}))
}

// NewTelegramProvider shares the HTTP client with a long-lived, cached JWKS
// verifier. Tests can exercise the real protocol using a local HTTP transport.
func NewTelegramProvider(client *http.Client) *TelegramProvider {
	return &TelegramProvider{
		client: client,
		keys: oidc.NewRemoteKeySet(
			oidc.ClientContext(context.Background(), client),
			TelegramIssuer+"/.well-known/jwks.json",
		),
	}
}

func (p *TelegramProvider) GetName() string { return "Telegram" }

func (p *TelegramProvider) IsEnabled() bool { return TelegramConfigurationError() == nil }

func (p *TelegramProvider) ExchangeToken(ctx context.Context, code string, c *gin.Context) (*OAuthToken, error) {
	if err := TelegramConfigurationError(); err != nil {
		return nil, err
	}
	value, _ := c.Get(TelegramOAuthFlowContextKey)
	flow, ok := value.(*TelegramOAuthFlow)
	settings := system_setting.GetTelegramSettings()
	if !ok || flow == nil || flow.CodeVerifier == "" || code == "" ||
		flow.ClientID != strings.TrimSpace(settings.ClientID) ||
		flow.RedirectURI != strings.TrimRight(system_setting.ServerAddress, "/")+"/oauth/telegram" {
		return nil, ErrTelegramOAuthFailed
	}
	values := url.Values{
		"grant_type":    {"authorization_code"},
		"code":          {code},
		"client_id":     {flow.ClientID},
		"redirect_uri":  {flow.RedirectURI},
		"code_verifier": {flow.CodeVerifier},
	}
	request, err := http.NewRequestWithContext(ctx, http.MethodPost, TelegramIssuer+"/token", strings.NewReader(values.Encode()))
	if err != nil {
		return nil, fmt.Errorf("%w: %v", ErrTelegramOAuthFailed, err)
	}
	request.SetBasicAuth(flow.ClientID, strings.TrimSpace(settings.ClientSecret))
	request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	response, err := p.client.Do(request)
	if err != nil {
		return nil, fmt.Errorf("%w: %v", ErrTelegramOAuthFailed, err)
	}
	defer response.Body.Close()
	if response.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("%w: token endpoint status %d", ErrTelegramOAuthFailed, response.StatusCode)
	}
	var token OAuthToken
	if err := common.DecodeJson(io.LimitReader(response.Body, 1<<20), &token); err != nil {
		return nil, fmt.Errorf("%w: %v", ErrTelegramOAuthFailed, err)
	}
	if token.IDToken == "" {
		return nil, ErrTelegramOAuthFailed
	}
	token.ClientID = flow.ClientID
	return &token, nil
}

func (p *TelegramProvider) GetUserInfo(ctx context.Context, token *OAuthToken) (*OAuthUser, error) {
	if err := TelegramConfigurationError(); err != nil {
		return nil, err
	}
	if token == nil || token.ClientID != strings.TrimSpace(system_setting.GetTelegramSettings().ClientID) {
		return nil, ErrTelegramOAuthFailed
	}
	verifier := oidc.NewVerifier(TelegramIssuer, p.keys, &oidc.Config{
		ClientID: token.ClientID, SupportedSigningAlgs: []string{oidc.RS256, oidc.ES256},
	})
	verified, err := verifier.Verify(ctx, token.IDToken)
	if err != nil {
		return nil, fmt.Errorf("%w: %v", ErrTelegramOAuthFailed, err)
	}
	var rawClaims json.RawMessage
	if err := verified.Claims(&rawClaims); err != nil {
		return nil, fmt.Errorf("%w: %v", ErrTelegramOAuthFailed, err)
	}
	var claims struct {
		ID       json.Number `json:"id"`
		Name     string      `json:"name"`
		Username string      `json:"preferred_username"`
	}
	if err := common.Unmarshal(rawClaims, &claims); err != nil {
		return nil, fmt.Errorf("%w: %v", ErrTelegramOAuthFailed, err)
	}
	id, err := strconv.ParseUint(claims.ID.String(), 10, 64)
	if err != nil || id == 0 || verified.Subject == "" {
		return nil, ErrTelegramOAuthFailed
	}
	return &OAuthUser{
		ProviderUserID: strconv.FormatUint(id, 10),
		Username:       claims.Username, DisplayName: claims.Name,
	}, nil
}

func (p *TelegramProvider) IsUserIDTaken(id string) bool { return model.IsTelegramIdAlreadyTaken(id) }

func (p *TelegramProvider) FillUserByProviderID(user *model.User, id string) error {
	stored, err := model.GetUserByTelegramID(id)
	if err != nil {
		return err
	}
	*user = *stored
	return nil
}

func (p *TelegramProvider) SetProviderUserID(user *model.User, id string) { user.TelegramId = id }

func (p *TelegramProvider) GetProviderPrefix() string { return "telegram_" }

func (p *TelegramProvider) ProviderUserIDColumn() string { return "telegram_id" }