Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ docker run --name new-api -d --restart always \
- 🤖 Connexion par autorisation LinuxDO
- 📱 Connexion par autorisation Telegram
- 🔑 Authentification unifiée OIDC
- Enterprise SSO (JWT Direct) : validation JWT directe, echange de ticket, UserInfo et flux CAS validate
- 🔍 Requête de quota d'utilisation de clé (avec [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool))

### 🚀 Fonctionnalités avancées
Expand Down
1 change: 1 addition & 0 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ docker run --name new-api -d --restart always \
- 🤖 LinuxDO認証ログイン
- 📱 Telegram認証ログイン
- 🔑 OIDC統一認証
- Enterprise SSO(JWT Direct): 直接JWT検証、チケット交換、UserInfo、CAS Validate フロー
- 🔍 Key使用量クォータ照会([neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool)と併用)


Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ docker run --name new-api -d --restart always \
- 🤖 LinuxDO authorization login
- 📱 Telegram authorization login
- 🔑 OIDC unified authentication
- Enterprise SSO (JWT Direct): direct JWT validation, ticket exchange, UserInfo, and CAS validate flows
- 🔍 Key quota query usage (with [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool))

### 🚀 Advanced Features
Expand Down
1 change: 1 addition & 0 deletions README.zh_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ docker run --name new-api -d --restart always \
- 🤖 LinuxDO 授权登录
- 📱 Telegram 授权登录
- 🔑 OIDC 统一认证
- 企业 SSO(JWT Direct):支持直验 JWT、票据换 JWT、UserInfo 和 CAS Validate 接入
- 🔍 Key 查询使用额度(配合 [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool))

### 🚀 高级功能
Expand Down
1 change: 1 addition & 0 deletions README.zh_TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ docker run --name new-api -d --restart always \
- 🤖 LinuxDO 授權登錄
- 📱 Telegram 授權登錄
- 🔑 OIDC 統一認證
- 企業 SSO(JWT Direct):支援直接驗證 JWT、票據換 JWT、UserInfo 與 CAS Validate 接入
- 🔍 Key 查詢使用額度(配合 [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool))

### 🚀 高級功能
Expand Down
2 changes: 2 additions & 0 deletions common/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"io"
)

type RawMessage = json.RawMessage

func Unmarshal(data []byte, v any) error {
return json.Unmarshal(data, v)
}
Expand Down
183 changes: 183 additions & 0 deletions controller/bind_session_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package controller

import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"sync/atomic"
"net/http"
"net/http/httptest"
"net/url"
"sort"
"strconv"
"testing"
"time"

"github.com/QuantumNous/new-api/common"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
)

func newBindSessionTestRouter(t *testing.T) *gin.Engine {
t.Helper()

router := gin.New()
store := cookie.NewStore([]byte("bind-session-test-secret"))
router.Use(sessions.Sessions("session", store))
router.GET("/api/oauth/email/bind", EmailBind)
router.GET("/api/oauth/wechat/bind", WeChatBind)
router.GET("/api/oauth/telegram/bind", TelegramBind)
return router
}

func performBindSessionTestRequest(t *testing.T, serverURL string, path string) oauthJWTAPIResponse {
t.Helper()

response, err := http.Get(serverURL + path)
if err != nil {
t.Fatalf("failed to perform request: %v", err)
}
defer response.Body.Close()

var payload oauthJWTAPIResponse
if err := common.DecodeJson(response.Body, &payload); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
return payload
}

func TestEmailBindRequiresLoggedInSession(t *testing.T) {
setupCustomOAuthJWTControllerTestDB(t)
router := newBindSessionTestRouter(t)
server := httptest.NewServer(router)
defer server.Close()

email := "bind-email@example.com"
code := "123456"
common.RegisterVerificationCodeWithKey(email, code, common.EmailVerificationPurpose)
t.Cleanup(func() {
common.DeleteKey(email, common.EmailVerificationPurpose)
})

response := performBindSessionTestRequest(
t,
server.URL,
"/api/oauth/email/bind?email="+url.QueryEscape(email)+"&code="+url.QueryEscape(code),
)

if response.Success {
t.Fatalf("expected email bind without session to fail")
}
if response.Message != "未登录" {
t.Fatalf("unexpected error message: %s", response.Message)
}
}

func TestWeChatBindRequiresLoggedInSession(t *testing.T) {
setupCustomOAuthJWTControllerTestDB(t)
router := newBindSessionTestRouter(t)
server := httptest.NewServer(router)
defer server.Close()

oldEnabled := common.WeChatAuthEnabled
oldAddress := common.WeChatServerAddress
oldToken := common.WeChatServerToken
defer func() {
common.WeChatAuthEnabled = oldEnabled
common.WeChatServerAddress = oldAddress
common.WeChatServerToken = oldToken
}()

var upstreamRequests int32
wechatServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt32(&upstreamRequests, 1)
_, _ = w.Write([]byte(`{"success":true,"message":"","data":"wechat-user-1"}`))
}))
defer wechatServer.Close()

common.WeChatAuthEnabled = true
common.WeChatServerAddress = wechatServer.URL
common.WeChatServerToken = "test-wechat-token"

response := performBindSessionTestRequest(
t,
server.URL,
"/api/oauth/wechat/bind?code=wechat-code",
)

if response.Success {
t.Fatalf("expected wechat bind without session to fail")
}
if response.Message != "未登录" {
t.Fatalf("unexpected error message: %s", response.Message)
}
if atomic.LoadInt32(&upstreamRequests) != 0 {
t.Fatalf("expected unauthenticated wechat bind to avoid upstream requests, got %d", atomic.LoadInt32(&upstreamRequests))
}
}

func TestTelegramBindRequiresLoggedInSession(t *testing.T) {
setupCustomOAuthJWTControllerTestDB(t)
router := newBindSessionTestRouter(t)
server := httptest.NewServer(router)
defer server.Close()

oldEnabled := common.TelegramOAuthEnabled
oldToken := common.TelegramBotToken
defer func() {
common.TelegramOAuthEnabled = oldEnabled
common.TelegramBotToken = oldToken
}()

common.TelegramOAuthEnabled = true
common.TelegramBotToken = "test-telegram-bot-token"

params := buildTelegramAuthParams(common.TelegramBotToken, "123456789")
response := performBindSessionTestRequest(
t,
server.URL,
"/api/oauth/telegram/bind?"+params.Encode(),
)

if response.Success {
t.Fatalf("expected telegram bind without session to fail")
}
if response.Message != "未登录" {
t.Fatalf("unexpected error message: %s", response.Message)
}
}

func buildTelegramAuthParams(botToken string, telegramID string) url.Values {
params := url.Values{}
params.Set("id", telegramID)
params.Set("first_name", "Bind")
params.Set("auth_date", strconv.FormatInt(time.Now().Unix(), 10))
params.Set("hash", telegramAuthHash(params, botToken))
return params
}

func telegramAuthHash(params url.Values, token string) string {
items := make([]string, 0, len(params))
for key, values := range params {
if key == "hash" || len(values) == 0 {
continue
}
items = append(items, key+"="+values[0])
}
sort.Strings(items)

payload := ""
for index, item := range items {
if index > 0 {
payload += "\n"
}
payload += item
}

sha256hash := sha256.New()
_, _ = sha256hash.Write([]byte(token))
hmacHash := hmac.New(sha256.New, sha256hash.Sum(nil))
_, _ = hmacHash.Write([]byte(payload))
return hex.EncodeToString(hmacHash.Sum(nil))
}
Loading