mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-10-28 08:35:33 +08:00
Use authorization code flow with PKCE and verified ID tokens for Telegram login, binding, and security verification. Preserve existing bindings and require administrator OAuth configuration. Keep the restricted WeChat first-enrollment session proof, fix missing-target authentication errors, and preserve callback requests after OAuth popups close.
203 lines
6.9 KiB
Go
203 lines
6.9 KiB
Go
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" }
|