465 lines
21 KiB
Go
465 lines
21 KiB
Go
package controller
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"math/big"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/QuantumNous/new-api/model"
|
|
"github.com/QuantumNous/new-api/oauth"
|
|
"github.com/QuantumNous/new-api/service"
|
|
"github.com/QuantumNous/new-api/setting/system_setting"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/oauth2"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type telegramTestTransport struct{ target *url.URL }
|
|
|
|
func (transport telegramTestTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
if request.URL.Host != "oauth.telegram.org" {
|
|
return nil, fmt.Errorf("unexpected OAuth host: %s", request.URL.Host)
|
|
}
|
|
clone := request.Clone(request.Context())
|
|
clone.URL.Scheme, clone.URL.Host = transport.target.Scheme, transport.target.Host
|
|
return http.DefaultTransport.RoundTrip(clone)
|
|
}
|
|
|
|
type telegramTestGrant struct {
|
|
challenge string
|
|
claims jwt.MapClaims
|
|
key *rsa.PrivateKey
|
|
}
|
|
|
|
type telegramOAuthFixture struct {
|
|
user *model.User
|
|
identity service.AuthIdentity
|
|
client *http.Client
|
|
key *rsa.PrivateKey
|
|
mutex sync.Mutex
|
|
grants map[string]telegramTestGrant
|
|
tokenStatus int
|
|
jwksStatus int
|
|
}
|
|
|
|
func setupTelegramOAuthTest(t *testing.T) *telegramOAuthFixture {
|
|
t.Helper()
|
|
user, identity := setupSecurityEnrollmentTest(t)
|
|
require.NoError(t, model.DB.AutoMigrate(&model.ExternalIdentityClaim{}, &model.Option{}))
|
|
previousEnabled := common.TelegramOAuthEnabled
|
|
previousSettings := *system_setting.GetTelegramSettings()
|
|
previousAddress := system_setting.ServerAddress
|
|
previousProvider := oauth.GetProvider("telegram")
|
|
common.OptionMapRWMutex.Lock()
|
|
previousOptions := common.OptionMap
|
|
common.OptionMap = make(map[string]string)
|
|
maps.Copy(common.OptionMap, previousOptions)
|
|
common.OptionMapRWMutex.Unlock()
|
|
common.TelegramOAuthEnabled = true
|
|
*system_setting.GetTelegramSettings() = system_setting.TelegramSettings{ClientID: "12345", ClientSecret: "telegram-client-secret"}
|
|
system_setting.ServerAddress = "https://example.com"
|
|
t.Cleanup(func() {
|
|
common.OptionMapRWMutex.Lock()
|
|
common.OptionMap = previousOptions
|
|
common.OptionMapRWMutex.Unlock()
|
|
common.TelegramOAuthEnabled = previousEnabled
|
|
*system_setting.GetTelegramSettings() = previousSettings
|
|
system_setting.ServerAddress = previousAddress
|
|
oauth.Register("telegram", previousProvider)
|
|
oauth.UnregisterCustomProvider("telegram")
|
|
})
|
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
require.NoError(t, err)
|
|
fixture := &telegramOAuthFixture{user: user, identity: identity, key: key, grants: map[string]telegramTestGrant{}}
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
|
|
fixture.mutex.Lock()
|
|
defer fixture.mutex.Unlock()
|
|
w.Header().Set("Content-Type", "application/json")
|
|
switch request.URL.Path {
|
|
case "/.well-known/jwks.json":
|
|
if fixture.jwksStatus != 0 {
|
|
w.WriteHeader(fixture.jwksStatus)
|
|
return
|
|
}
|
|
payload, err := common.Marshal(map[string]any{"keys": []any{map[string]any{
|
|
"kty": "RSA", "kid": "telegram-test-key", "alg": "RS256", "use": "sig",
|
|
"n": base64.RawURLEncoding.EncodeToString(key.N.Bytes()),
|
|
"e": base64.RawURLEncoding.EncodeToString(big.NewInt(int64(key.E)).Bytes()),
|
|
}}})
|
|
if assert.NoError(t, err) {
|
|
_, _ = w.Write(payload)
|
|
}
|
|
case "/token":
|
|
if fixture.tokenStatus != 0 {
|
|
w.WriteHeader(fixture.tokenStatus)
|
|
return
|
|
}
|
|
clientID, secret, ok := request.BasicAuth()
|
|
if !ok || clientID != "12345" || secret != "telegram-client-secret" {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
if request.ParseForm() != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
code := request.Form.Get("code")
|
|
grant, ok := fixture.grants[code]
|
|
if !ok || request.Form.Get("grant_type") != "authorization_code" ||
|
|
request.Form.Get("client_id") != "12345" || request.Form.Get("redirect_uri") != "https://example.com/oauth/telegram" ||
|
|
oauth2.S256ChallengeFromVerifier(request.Form.Get("code_verifier")) != grant.challenge {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
delete(fixture.grants, code)
|
|
token := jwt.NewWithClaims(jwt.SigningMethodRS256, grant.claims)
|
|
token.Header["kid"] = "telegram-test-key"
|
|
signed, err := token.SignedString(grant.key)
|
|
if !assert.NoError(t, err) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
payload, err := common.Marshal(map[string]any{"access_token": "test-access-token", "id_token": signed, "token_type": "Bearer"})
|
|
if assert.NoError(t, err) {
|
|
_, _ = w.Write(payload)
|
|
}
|
|
default:
|
|
t.Errorf("unexpected Telegram endpoint: %s", request.URL.Path)
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
t.Cleanup(server.Close)
|
|
target, err := url.Parse(server.URL)
|
|
require.NoError(t, err)
|
|
fixture.client = &http.Client{Transport: telegramTestTransport{target: target}, Timeout: 5 * time.Second}
|
|
oauth.Register("telegram", oauth.NewTelegramProvider(fixture.client))
|
|
return fixture
|
|
}
|
|
|
|
func (fixture *telegramOAuthFixture) authorization(t *testing.T, intent string, identity service.AuthIdentity, scope string, claims jwt.MapClaims) (string, string) {
|
|
t.Helper()
|
|
request, err := common.Marshal(oauthStateRequest{Provider: "telegram", Intent: intent, Scope: scope})
|
|
require.NoError(t, err)
|
|
proof := ""
|
|
if intent == "bind" {
|
|
proof = issueSecurityEnrollmentProof(t, identity, service.VerificationOperation{Scope: service.VerificationScopeAccountBind, Context: []byte(`{"provider":"telegram"}`)}, service.VerificationMethodPassword)
|
|
}
|
|
response := securityEnrollmentRequest("POST", "/api/oauth/state", string(request), proof, identity, GenerateOAuthCode)
|
|
var body securityEnrollmentResponse
|
|
require.NoError(t, common.Unmarshal(response.Body.Bytes(), &body))
|
|
require.True(t, body.Success, response.Body.String())
|
|
var data struct {
|
|
State string `json:"flow_token"`
|
|
URL string `json:"authorization_url"`
|
|
}
|
|
require.NoError(t, common.Unmarshal(body.Data, &data))
|
|
authorizationURL, err := url.Parse(data.URL)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "https://oauth.telegram.org/auth", authorizationURL.Scheme+"://"+authorizationURL.Host+authorizationURL.Path)
|
|
assert.Equal(t, "openid profile", authorizationURL.Query().Get("scope"))
|
|
assert.Equal(t, "S256", authorizationURL.Query().Get("code_challenge_method"))
|
|
assert.Equal(t, data.State, authorizationURL.Query().Get("state"))
|
|
assert.Empty(t, authorizationURL.Query().Get("code_verifier"))
|
|
assert.NotContains(t, response.Body.String(), "telegram-client-secret")
|
|
code := "code-" + data.State
|
|
fixture.mutex.Lock()
|
|
fixture.grants[code] = telegramTestGrant{challenge: authorizationURL.Query().Get("code_challenge"), claims: claims, key: fixture.key}
|
|
fixture.mutex.Unlock()
|
|
return data.State, code
|
|
}
|
|
|
|
func telegramIdentityClaims(id any) jwt.MapClaims {
|
|
return jwt.MapClaims{
|
|
"iss": oauth.TelegramIssuer, "aud": "12345", "sub": "different-oidc-subject",
|
|
"iat": time.Now().Unix(), "exp": time.Now().Add(time.Minute).Unix(),
|
|
"id": id, "name": "Telegram User", "preferred_username": "telegram-user",
|
|
}
|
|
}
|
|
|
|
func telegramOAuthCallback(state, code string, identity service.AuthIdentity) *httptest.ResponseRecorder {
|
|
path := "/api/oauth/telegram?" + url.Values{"state": {state}, "code": {code}}.Encode()
|
|
return securityEnrollmentRequest("GET", path, "", "", identity, func(c *gin.Context) {
|
|
c.Params = gin.Params{{Key: "provider", Value: "telegram"}}
|
|
HandleOAuth(c)
|
|
})
|
|
}
|
|
|
|
func TestTelegramOAuthPreservesExistingAccountAndBinding(t *testing.T) {
|
|
fixture := setupTelegramOAuthTest(t)
|
|
const telegramID = "1234567890123456"
|
|
require.NoError(t, model.DB.Model(fixture.user).Update("telegram_id", telegramID).Error)
|
|
require.NoError(t, model.InitializeExternalIdentityClaims())
|
|
require.NoError(t, model.InitializeExternalIdentityClaims())
|
|
state, code := fixture.authorization(t, "login", service.AuthIdentity{}, "", telegramIdentityClaims(json.Number(telegramID)))
|
|
response := telegramOAuthCallback(state, code, service.AuthIdentity{})
|
|
var body securityEnrollmentResponse
|
|
require.NoError(t, common.Unmarshal(response.Body.Bytes(), &body))
|
|
require.True(t, body.Success, response.Body.String())
|
|
var login struct {
|
|
User struct {
|
|
ID int `json:"id"`
|
|
} `json:"user"`
|
|
}
|
|
require.NoError(t, common.Unmarshal(body.Data, &login))
|
|
assert.Equal(t, fixture.user.Id, login.User.ID)
|
|
stored, err := model.GetUserByTelegramID(telegramID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, telegramID, stored.TelegramId)
|
|
var claim model.ExternalIdentityClaim
|
|
require.NoError(t, model.DB.Where("provider = ? AND subject = ?", "telegram", telegramID).First(&claim).Error)
|
|
assert.Equal(t, stored.Id, claim.UserId)
|
|
replay := telegramOAuthCallback(state, code, service.AuthIdentity{})
|
|
assert.Equal(t, http.StatusForbidden, replay.Code)
|
|
state, code = fixture.authorization(t, "login", service.AuthIdentity{}, "", telegramIdentityClaims(999))
|
|
response = telegramOAuthCallback(state, code, service.AuthIdentity{})
|
|
assert.Contains(t, response.Body.String(), "TELEGRAM_ACCOUNT_NOT_BOUND")
|
|
var count int64
|
|
require.NoError(t, model.DB.Model(&model.User{}).Count(&count).Error)
|
|
assert.EqualValues(t, 1, count)
|
|
}
|
|
|
|
func TestTelegramOAuthRejectsInvalidTokens(t *testing.T) {
|
|
fixture := setupTelegramOAuthTest(t)
|
|
require.NoError(t, model.DB.Model(fixture.user).Update("telegram_id", "42").Error)
|
|
for _, test := range []struct {
|
|
name string
|
|
change func(jwt.MapClaims)
|
|
}{
|
|
{"issuer", func(c jwt.MapClaims) { c["iss"] = "https://other.example" }},
|
|
{"audience", func(c jwt.MapClaims) { c["aud"] = "other-client" }},
|
|
{"expired", func(c jwt.MapClaims) { c["exp"] = time.Now().Add(-time.Minute).Unix() }},
|
|
{"missing id", func(c jwt.MapClaims) { delete(c, "id") }},
|
|
{"negative id", func(c jwt.MapClaims) { c["id"] = -1 }},
|
|
{"fractional id", func(c jwt.MapClaims) { c["id"] = 1.5 }},
|
|
{"overflow id", func(c jwt.MapClaims) { c["id"] = "18446744073709551616" }},
|
|
{"missing subject", func(c jwt.MapClaims) { delete(c, "sub") }},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
claims := telegramIdentityClaims(42)
|
|
test.change(claims)
|
|
state, code := fixture.authorization(t, "login", service.AuthIdentity{}, "", claims)
|
|
response := telegramOAuthCallback(state, code, service.AuthIdentity{})
|
|
assert.Contains(t, response.Body.String(), "TELEGRAM_OAUTH_FAILED")
|
|
assert.NotContains(t, response.Body.String(), "access_token")
|
|
})
|
|
}
|
|
t.Run("signature", func(t *testing.T) {
|
|
state, code := fixture.authorization(t, "login", service.AuthIdentity{}, "", telegramIdentityClaims(42))
|
|
otherKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
require.NoError(t, err)
|
|
grant := fixture.grants[code]
|
|
grant.key = otherKey
|
|
fixture.grants[code] = grant
|
|
assert.Contains(t, telegramOAuthCallback(state, code, service.AuthIdentity{}).Body.String(), "TELEGRAM_OAUTH_FAILED")
|
|
})
|
|
t.Run("PKCE", func(t *testing.T) {
|
|
state, code := fixture.authorization(t, "login", service.AuthIdentity{}, "", telegramIdentityClaims(42))
|
|
grant := fixture.grants[code]
|
|
grant.challenge = "wrong-challenge"
|
|
fixture.grants[code] = grant
|
|
assert.Contains(t, telegramOAuthCallback(state, code, service.AuthIdentity{}).Body.String(), "TELEGRAM_OAUTH_FAILED")
|
|
})
|
|
for _, endpoint := range []string{"token", "jwks"} {
|
|
t.Run(endpoint+" unavailable", func(t *testing.T) {
|
|
state, code := fixture.authorization(t, "login", service.AuthIdentity{}, "", telegramIdentityClaims(42))
|
|
oauth.Register("telegram", oauth.NewTelegramProvider(fixture.client))
|
|
if endpoint == "token" {
|
|
fixture.tokenStatus = 503
|
|
} else {
|
|
fixture.jwksStatus = 503
|
|
}
|
|
response := telegramOAuthCallback(state, code, service.AuthIdentity{})
|
|
assert.Contains(t, response.Body.String(), "TELEGRAM_OAUTH_FAILED")
|
|
fixture.tokenStatus, fixture.jwksStatus = 0, 0
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTelegramOAuthBindingIsAtomicAndSessionBound(t *testing.T) {
|
|
fixture := setupTelegramOAuthTest(t)
|
|
// Older rows may predate the Telegram column and have a NULL binding.
|
|
require.NoError(t, model.DB.Model(fixture.user).Update("telegram_id", nil).Error)
|
|
state, code := fixture.authorization(t, "bind", fixture.identity, "", telegramIdentityClaims(42))
|
|
otherBundle, err := service.CreateLoginSession(fixture.user.Id, "password", "127.0.0.1", "other")
|
|
require.NoError(t, err)
|
|
otherIdentity, err := service.ParseAccessToken(otherBundle.AccessToken)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, http.StatusForbidden, telegramOAuthCallback(state, code, otherIdentity).Code)
|
|
failure := errors.New("private Telegram bind storage failure")
|
|
require.NoError(t, model.DB.Callback().Update().Before("gorm:update").Register("telegram_bind_failure", func(tx *gorm.DB) {
|
|
if tx.Statement.Table == "users" {
|
|
tx.AddError(failure)
|
|
}
|
|
}))
|
|
response := telegramOAuthCallback(state, code, fixture.identity)
|
|
assert.Equal(t, http.StatusInternalServerError, response.Code)
|
|
assert.NotContains(t, response.Body.String(), "private")
|
|
require.NoError(t, model.DB.Callback().Update().Remove("telegram_bind_failure"))
|
|
_, err = model.GetAuthFlow(state, model.AuthFlowMatch{Purpose: model.AuthFlowPurposeOAuth, Provider: "telegram"})
|
|
require.NoError(t, err)
|
|
var claimCount int64
|
|
require.NoError(t, model.DB.Model(&model.ExternalIdentityClaim{}).Count(&claimCount).Error)
|
|
assert.Zero(t, claimCount)
|
|
state, code = fixture.authorization(t, "bind", fixture.identity, "", telegramIdentityClaims(42))
|
|
response = telegramOAuthCallback(state, code, fixture.identity)
|
|
assert.Contains(t, response.Body.String(), `"success":true`)
|
|
stored, err := model.GetUserById(fixture.user.Id, false)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "42", stored.TelegramId)
|
|
assert.Equal(t, fixture.user.Role, stored.Role)
|
|
assert.Equal(t, fixture.user.Status, stored.Status)
|
|
assert.Equal(t, fixture.user.AuthVersion, stored.AuthVersion)
|
|
assert.Equal(t, http.StatusForbidden, telegramOAuthCallback(state, code, fixture.identity).Code)
|
|
}
|
|
|
|
func TestTelegramOAuthBindingRejectsChangedAccountsAndDuplicateOwnership(t *testing.T) {
|
|
for _, change := range []string{"revoked", "disabled", "deleted", "already bound", "owned by another user"} {
|
|
t.Run(change, func(t *testing.T) {
|
|
fixture := setupTelegramOAuthTest(t)
|
|
state, code := fixture.authorization(t, "bind", fixture.identity, "", telegramIdentityClaims(42))
|
|
switch change {
|
|
case "revoked":
|
|
_, err := model.RevokeAllUserSessions(fixture.user.Id, "test")
|
|
require.NoError(t, err)
|
|
case "disabled":
|
|
require.NoError(t, model.DB.Model(fixture.user).Update("status", common.UserStatusDisabled).Error)
|
|
case "deleted":
|
|
require.NoError(t, model.DB.Delete(fixture.user).Error)
|
|
case "already bound":
|
|
require.NoError(t, model.DB.Model(fixture.user).Update("telegram_id", "99").Error)
|
|
require.NoError(t, model.InitializeExternalIdentityClaims())
|
|
case "owned by another user":
|
|
owner := &model.User{Username: "owner", AffCode: "owner", Status: common.UserStatusEnabled, AuthVersion: 1, TelegramId: "42"}
|
|
require.NoError(t, model.DB.Create(owner).Error)
|
|
require.NoError(t, model.InitializeExternalIdentityClaims())
|
|
}
|
|
response := telegramOAuthCallback(state, code, fixture.identity)
|
|
assert.Contains(t, response.Body.String(), `"success":false`)
|
|
var claims int64
|
|
require.NoError(t, model.DB.Model(&model.ExternalIdentityClaim{}).Where("user_id = ? AND subject = ?", fixture.user.Id, "42").Count(&claims).Error)
|
|
assert.Zero(t, claims)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTelegramOAuthConfigurationAndLegacyEndpoints(t *testing.T) {
|
|
fixture := setupTelegramOAuthTest(t)
|
|
previousBotToken := common.TelegramBotToken
|
|
t.Cleanup(func() { common.TelegramBotToken = previousBotToken })
|
|
require.NoError(t, model.UpdateOptionsBulk(map[string]string{
|
|
"telegram.client_id": "12345", "telegram.client_secret": "telegram-client-secret",
|
|
"TelegramBotToken": "stored-legacy-token", "TelegramOAuthEnabled": "true",
|
|
}))
|
|
for range 2 {
|
|
*system_setting.GetTelegramSettings() = system_setting.TelegramSettings{}
|
|
common.TelegramOAuthEnabled = false
|
|
model.InitOptionMap()
|
|
assert.True(t, common.TelegramOAuthEnabled)
|
|
assert.True(t, system_setting.GetTelegramSettings().IsConfigured())
|
|
assert.Equal(t, "telegram-client-secret", system_setting.GetTelegramSettings().ClientSecret)
|
|
assert.Equal(t, "stored-legacy-token", common.TelegramBotToken)
|
|
}
|
|
var stored model.Option
|
|
require.NoError(t, model.DB.Where(&model.Option{Key: "telegram.client_id"}).First(&stored).Error)
|
|
assert.Equal(t, "12345", stored.Value)
|
|
status := securityEnrollmentRequest("GET", "/api/status", "", "", fixture.identity, GetStatus)
|
|
assert.Contains(t, status.Body.String(), `"telegram_oauth_configured":true`)
|
|
assert.NotContains(t, status.Body.String(), "telegram-client-secret")
|
|
options := securityEnrollmentRequest("GET", "/api/option/", "", "", fixture.identity, GetOptions)
|
|
assert.NotContains(t, options.Body.String(), "telegram-client-secret")
|
|
assert.NotContains(t, options.Body.String(), "stored-legacy-token")
|
|
require.NoError(t, model.DB.Model(fixture.user).Updates(map[string]any{"password": "", "telegram_id": "42"}).Error)
|
|
for _, unavailable := range []string{"missing secret", "disabled", "conflict"} {
|
|
t.Run(unavailable, func(t *testing.T) {
|
|
switch unavailable {
|
|
case "missing secret":
|
|
system_setting.GetTelegramSettings().ClientSecret = ""
|
|
case "disabled":
|
|
common.TelegramOAuthEnabled = false
|
|
case "conflict":
|
|
require.Error(t, oauth.RegisterCustom("telegram", oauth.NewGenericOAuthProvider(&model.CustomOAuthProvider{Slug: "telegram"})))
|
|
}
|
|
response := securityEnrollmentRequest("POST", "/api/oauth/state", `{"provider":"telegram","intent":"login"}`, "", service.AuthIdentity{}, GenerateOAuthCode)
|
|
assert.Contains(t, response.Body.String(), `"success":false`)
|
|
assert.NotContains(t, response.Body.String(), "flow_token")
|
|
for _, scope := range []string{"2fa.setup", "passkey.register"} {
|
|
requirements, err := service.GetVerificationRequirements(fixture.identity, scope)
|
|
require.NoError(t, err)
|
|
require.Len(t, requirements.Methods, 1)
|
|
assert.False(t, requirements.Methods[0].Available)
|
|
assert.Contains(t, requirements.Methods[0].Reason, "administrator")
|
|
_, err = service.VerifySecurityInput(fixture.identity, service.VerificationInput{Scope: scope, Method: "session"})
|
|
assert.Error(t, err)
|
|
_, err = service.StartOAuthVerification(fixture.identity, service.VerificationOperation{Scope: scope}, "telegram")
|
|
assert.Error(t, err)
|
|
}
|
|
for _, handler := range []gin.HandlerFunc{Setup2FA, PasskeyRegisterBegin} {
|
|
response := securityEnrollmentRequest("POST", "/setup", `{}`, "", fixture.identity, handler)
|
|
assert.Contains(t, response.Body.String(), `"success":false`)
|
|
assert.NotContains(t, response.Body.String(), "flow_token")
|
|
}
|
|
common.TelegramOAuthEnabled = true
|
|
system_setting.GetTelegramSettings().ClientSecret = "telegram-client-secret"
|
|
oauth.UnregisterCustomProvider("telegram")
|
|
})
|
|
}
|
|
for _, endpoint := range []struct{ method, path string }{
|
|
{"GET", "/api/oauth/telegram/login"},
|
|
{"POST", "/api/oauth/telegram/bind/start"},
|
|
{"GET", "/api/oauth/telegram/bind/old-flow"},
|
|
} {
|
|
response := securityEnrollmentRequest(endpoint.method, endpoint.path, "", "", fixture.identity, TelegramLegacyAuth)
|
|
assert.Equal(t, http.StatusGone, response.Code)
|
|
assert.Contains(t, response.Body.String(), "TELEGRAM_LEGACY_AUTH_REMOVED")
|
|
}
|
|
}
|
|
|
|
func TestTelegramOAuthConcurrentBindingHasSingleOwner(t *testing.T) {
|
|
fixture := setupTelegramOAuthTest(t)
|
|
other := &model.User{Username: "other-owner", AffCode: "other-owner", Password: fixture.user.Password, Status: common.UserStatusEnabled, AuthVersion: 1}
|
|
require.NoError(t, model.DB.Create(other).Error)
|
|
bundle, err := service.CreateLoginSession(other.Id, "password", "127.0.0.1", "test")
|
|
require.NoError(t, err)
|
|
otherIdentity, err := service.ParseAccessToken(bundle.AccessToken)
|
|
require.NoError(t, err)
|
|
state, code := fixture.authorization(t, "bind", fixture.identity, "", telegramIdentityClaims(42))
|
|
otherState, otherCode := fixture.authorization(t, "bind", otherIdentity, "", telegramIdentityClaims(42))
|
|
start := make(chan struct{})
|
|
responses := make(chan *httptest.ResponseRecorder, 2)
|
|
go func() { <-start; responses <- telegramOAuthCallback(state, code, fixture.identity) }()
|
|
go func() { <-start; responses <- telegramOAuthCallback(otherState, otherCode, otherIdentity) }()
|
|
close(start)
|
|
successes := 0
|
|
for range 2 {
|
|
response := <-responses
|
|
var body securityEnrollmentResponse
|
|
require.NoError(t, common.Unmarshal(response.Body.Bytes(), &body))
|
|
if body.Success {
|
|
successes++
|
|
}
|
|
}
|
|
assert.Equal(t, 1, successes)
|
|
var owners []model.User
|
|
require.NoError(t, model.DB.Where("telegram_id = ?", "42").Find(&owners).Error)
|
|
require.Len(t, owners, 1)
|
|
var claims []model.ExternalIdentityClaim
|
|
require.NoError(t, model.DB.Where("provider = ? AND subject = ?", "telegram", "42").Find(&claims).Error)
|
|
require.Len(t, claims, 1)
|
|
assert.Equal(t, owners[0].Id, claims[0].UserId)
|
|
}
|