Files
new-api/controller/telegram.go
T
Seefs b6b97a66e3 fix: purge authentication data on hard user deletion (#6168)
* fix: purge authentication data on hard user deletion

* fix: fail closed when 2FA status lookup fails

* fix: reject stale Telegram login callbacks

* fix(twofa): prevent concurrent backup code and lockout bypasses

* fix(auth): harden user deletion and Telegram verification
2026-07-14 14:25:54 +08:00

157 lines
3.8 KiB
Go

package controller
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"errors"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"time"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
const (
// The legacy Telegram widget has no nonce. Keep its signed assertion short-lived
// so captured callbacks cannot be reused indefinitely.
telegramAuthorizationMaxAge = 5 * time.Minute
telegramAuthorizationFutureSkew = 2 * time.Minute
)
func TelegramBind(c *gin.Context) {
if !common.TelegramOAuthEnabled {
c.JSON(200, gin.H{
"message": "管理员未开启通过 Telegram 登录以及注册",
"success": false,
})
return
}
params := c.Request.URL.Query()
telegramId, err := verifyTelegramAuthorization(params, common.TelegramBotToken, time.Now())
if err != nil {
common.SysLog("TelegramBind authorization failed: " + err.Error())
c.JSON(200, gin.H{
"message": "无效的请求",
"success": false,
})
return
}
if model.IsTelegramIdAlreadyTaken(telegramId) {
c.JSON(200, gin.H{
"message": "该 Telegram 账户已被绑定",
"success": false,
})
return
}
session := sessions.Default(c)
id := session.Get("id")
user := model.User{Id: id.(int)}
if err := user.FillUserById(); err != nil {
c.JSON(200, gin.H{
"message": err.Error(),
"success": false,
})
return
}
if user.Id == 0 {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "用户已注销",
})
return
}
user.TelegramId = telegramId
if err := user.Update(false); err != nil {
c.JSON(200, gin.H{
"message": err.Error(),
"success": false,
})
return
}
c.Redirect(302, common.ThemeAwarePath("/console/personal"))
}
func TelegramLogin(c *gin.Context) {
if !common.TelegramOAuthEnabled {
c.JSON(200, gin.H{
"message": "管理员未开启通过 Telegram 登录以及注册",
"success": false,
})
return
}
params := c.Request.URL.Query()
telegramId, err := verifyTelegramAuthorization(params, common.TelegramBotToken, time.Now())
if err != nil {
common.SysLog("TelegramLogin authorization failed: " + err.Error())
c.JSON(200, gin.H{
"message": "无效的请求",
"success": false,
})
return
}
user := model.User{TelegramId: telegramId}
if err := user.FillUserByTelegramId(); err != nil {
c.JSON(200, gin.H{
"message": err.Error(),
"success": false,
})
return
}
setupLogin(&user, c)
}
func verifyTelegramAuthorization(params url.Values, token string, now time.Time) (string, error) {
if token == "" {
return "", errors.New("telegram bot token is empty")
}
for _, values := range params {
if len(values) != 1 {
return "", errors.New("telegram authorization contains duplicate parameters")
}
}
telegramID := params.Get("id")
hash := params.Get("hash")
authDateText := params.Get("auth_date")
if telegramID == "" || hash == "" || authDateText == "" {
return "", errors.New("telegram authorization is incomplete")
}
authDate, err := strconv.ParseInt(authDateText, 10, 64)
if err != nil {
return "", errors.New("telegram authorization date is invalid")
}
if authDate < now.Add(-telegramAuthorizationMaxAge).Unix() ||
authDate > now.Add(telegramAuthorizationFutureSkew).Unix() {
return "", errors.New("telegram authorization has expired")
}
strs := make([]string, 0, len(params)-1)
for k, v := range params {
if k == "hash" {
continue
}
strs = append(strs, k+"="+v[0])
}
sort.Strings(strs)
secret := sha256.Sum256([]byte(token))
mac := hmac.New(sha256.New, secret[:])
_, _ = mac.Write([]byte(strings.Join(strs, "\n")))
providedHash, err := hex.DecodeString(hash)
if err != nil || !hmac.Equal(providedHash, mac.Sum(nil)) {
return "", errors.New("telegram authorization signature is invalid")
}
return telegramID, nil
}