fix: sync temp auth key expiry boundaries
This commit is contained in:
parent
305e8a0008
commit
20a310f6ca
50 changed files with 3626 additions and 335 deletions
|
|
@ -77,7 +77,12 @@ func (r *Router) onAuthBindTempAuthKey(ctx context.Context, req *tg.AuthBindTemp
|
|||
r.tempKeyResolveCache.Delete(id)
|
||||
}
|
||||
if r.deps.Sessions != nil {
|
||||
r.deps.Sessions.BindAuthKeyForSession(id, sessionID, authKeyIDFromInt64(req.PermAuthKeyID))
|
||||
permID := authKeyIDFromInt64(req.PermAuthKeyID)
|
||||
if all, ok := r.deps.Sessions.(RawAuthKeySessionBinder); ok {
|
||||
all.BindAuthKeyForRawAuthKey(id, permID)
|
||||
} else {
|
||||
r.deps.Sessions.BindAuthKeyForSession(id, sessionID, permID)
|
||||
}
|
||||
}
|
||||
r.invalidateAuthUserCache(id)
|
||||
return true, nil
|
||||
|
|
|
|||
|
|
@ -65,6 +65,20 @@ type SessionBinder interface {
|
|||
PushToUserExceptAuthKeySession(ctx context.Context, userID int64, excludeAuthKeyID [8]byte, excludeSessionID int64, t proto.MessageType, msg bin.Encoder) (int, error)
|
||||
}
|
||||
|
||||
// RawAuthKeySessionBinder 在 auth.bindTempAuthKey 成功后,把同一 raw temporary key
|
||||
// 已建立的所有 session 一次性切到 canonical permanent identity。只更新当前 session
|
||||
// 会让并发启动的其它连接永久粘在 raw identity。
|
||||
type RawAuthKeySessionBinder interface {
|
||||
BindAuthKeyForRawAuthKey(rawAuthKeyID [8]byte, authKeyID [8]byte) int
|
||||
}
|
||||
|
||||
// RawAuthKeyMetadataProvider 暴露握手时确定的 raw key protocol expiry。0 表示
|
||||
// permanent key;正值表示 temporary key。Router 只用它判断 cached==raw 是否可作为
|
||||
// permanent 快路径,协议过期的实际拒绝仍由 mtprotoedge 完成。
|
||||
type RawAuthKeyMetadataProvider interface {
|
||||
AuthKeyExpiresAtForSession(rawAuthKeyID [8]byte, sessionID int64) (expiresAt int, found bool)
|
||||
}
|
||||
|
||||
// ImmediateSessionPusher 是可选的登录前信号直推能力。
|
||||
// 它绕过登录后 updates-ready 队列,只能用于会解锁登录流程本身的握手消息,
|
||||
// 例如 updateLoginToken。
|
||||
|
|
|
|||
|
|
@ -399,6 +399,8 @@ func signInErr(err error) error {
|
|||
return tgerr.New(400, "PHONE_CODE_INVALID")
|
||||
case errors.Is(err, auth.ErrCodeExpired):
|
||||
return tgerr.New(400, "PHONE_CODE_EXPIRED")
|
||||
case errors.Is(err, auth.ErrAuthKeyPermEmpty):
|
||||
return tgerr.New(401, "AUTH_KEY_PERM_EMPTY")
|
||||
case errors.Is(err, domain.ErrFirstNameInvalid):
|
||||
return firstNameInvalidErr()
|
||||
case errors.Is(err, domain.ErrSessionPasswordNeeded):
|
||||
|
|
@ -455,8 +457,14 @@ func passwordErr(err error) error {
|
|||
// bindTempAuthKeyErr 映射 PFS temp auth key 绑定错误。
|
||||
func bindTempAuthKeyErr(err error) error {
|
||||
switch {
|
||||
case errors.Is(err, auth.ErrExpiresAtInvalid):
|
||||
return tgerr.New(400, "EXPIRES_AT_INVALID")
|
||||
case errors.Is(err, auth.ErrTempAuthKeyEmpty):
|
||||
return tgerr.New(400, "TEMP_AUTH_KEY_EMPTY")
|
||||
case errors.Is(err, auth.ErrEncryptedMessageInvalid):
|
||||
return tgerr.New(400, "ENCRYPTED_MESSAGE_INVALID")
|
||||
case errors.Is(err, auth.ErrTempAuthKeyAlreadyBound):
|
||||
return tgerr.New(400, "TEMP_AUTH_KEY_ALREADY_BOUND")
|
||||
default:
|
||||
return internalErr()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
|
||||
"github.com/gotd/td/tgerr"
|
||||
|
||||
"telesrv/internal/app/auth"
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
|
|
@ -13,3 +14,22 @@ func TestPasswordErrMapsOccupiedLoginEmailToNotAllowed(t *testing.T) {
|
|||
t.Fatalf("passwordErr(ErrEmailOccupied) = %v, want EMAIL_NOT_ALLOWED", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindTempAuthKeyErrPreservesRecoverableRotationErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
err error
|
||||
want string
|
||||
}{
|
||||
{err: auth.ErrExpiresAtInvalid, want: "EXPIRES_AT_INVALID"},
|
||||
{err: auth.ErrTempAuthKeyEmpty, want: "TEMP_AUTH_KEY_EMPTY"},
|
||||
{err: auth.ErrEncryptedMessageInvalid, want: "ENCRYPTED_MESSAGE_INVALID"},
|
||||
{err: auth.ErrTempAuthKeyAlreadyBound, want: "TEMP_AUTH_KEY_ALREADY_BOUND"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.want, func(t *testing.T) {
|
||||
if err := bindTempAuthKeyErr(test.err); !tgerr.Is(err, test.want) {
|
||||
t.Fatalf("bindTempAuthKeyErr(%v) = %v, want %s", test.err, err, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -324,9 +324,25 @@ func (r *Router) effectiveAuthKeyID(ctx context.Context, rawAuthKeyID [8]byte, s
|
|||
}
|
||||
}
|
||||
if hasCached {
|
||||
if cached == rawAuthKeyID || r.deps.Auth == nil {
|
||||
if r.deps.Auth == nil {
|
||||
return cached, nil
|
||||
}
|
||||
if cached == rawAuthKeyID {
|
||||
// raw==business is a permanent-key fast path only when the edge confirms
|
||||
// the raw key has no protocol expiry. A temporary session may have cached
|
||||
// raw before another concurrent session completes auth.bindTempAuthKey;
|
||||
// it must keep resolving until the durable binding becomes visible.
|
||||
metadata, ok := r.deps.Sessions.(RawAuthKeyMetadataProvider)
|
||||
if ok {
|
||||
expiresAt, found := metadata.AuthKeyExpiresAtForSession(rawAuthKeyID, sessionID)
|
||||
if found && expiresAt == 0 {
|
||||
return cached, nil
|
||||
}
|
||||
}
|
||||
// Missing metadata and a session lookup miss both fail closed to the
|
||||
// durable resolver. Treating either as proof of permanence recreates the
|
||||
// raw-temp identity split when an alternate SessionBinder is installed.
|
||||
}
|
||||
// temp→perm 解析缓存:PFS 连接每帧都要解析一次 temp key(ResolveAuthKey 打 PG)。TTL 内复用
|
||||
// 上次解析、跳过 DB。仅当缓存的 perm 仍等于 session binder 当前 perm 才用(rebind 会改 binder
|
||||
// 且 onAuthBindTempAuthKey / 授权撤销都会显式 Delete 缓存,双保险防跨账号串号和被踢滞后)。
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import (
|
|||
appauth "telesrv/internal/app/auth"
|
||||
appdialogs "telesrv/internal/app/dialogs"
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store"
|
||||
"telesrv/internal/store/memory"
|
||||
"testing"
|
||||
"time"
|
||||
|
|
@ -913,17 +914,25 @@ func TestDispatchUnknownReturnsError(t *testing.T) {
|
|||
func TestDispatchResolvesBoundTempAuthKey(t *testing.T) {
|
||||
var tempAuthKeyID = [8]byte{0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55}
|
||||
var permAuthKeyID = [8]byte{0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11}
|
||||
tempBindings := memory.NewTempAuthKeyBindingStore()
|
||||
expiresAt := int(time.Now().Add(time.Hour).Unix())
|
||||
authKeys := memory.NewAuthKeyStore()
|
||||
if err := authKeys.Save(context.Background(), store.AuthKeyData{ID: tempAuthKeyID, ExpiresAt: expiresAt}); err != nil {
|
||||
t.Fatalf("save temporary auth key: %v", err)
|
||||
}
|
||||
if err := authKeys.Save(context.Background(), store.AuthKeyData{ID: permAuthKeyID}); err != nil {
|
||||
t.Fatalf("save permanent auth key: %v", err)
|
||||
}
|
||||
tempBindings := memory.NewTempAuthKeyBindingStore(authKeys)
|
||||
if err := tempBindings.Save(context.Background(), domain.TempAuthKeyBinding{
|
||||
TempAuthKeyID: tempAuthKeyID,
|
||||
PermAuthKeyID: int64(binary.LittleEndian.Uint64(permAuthKeyID[:])),
|
||||
ExpiresAt: int(time.Now().Add(time.Hour).Unix()),
|
||||
ExpiresAt: expiresAt,
|
||||
}); err != nil {
|
||||
t.Fatalf("save temp binding: %v", err)
|
||||
}
|
||||
sessions := &captureSessions{}
|
||||
r := New(Config{}, Deps{
|
||||
Auth: appauth.NewService(nil, nil, nil, nil, tempBindings, "12345"),
|
||||
Auth: appauth.NewService(nil, nil, nil, authKeys, tempBindings, "12345"),
|
||||
Sessions: sessions,
|
||||
}, zaptest.NewLogger(t), clock.System)
|
||||
req := &tg.HelpGetConfigRequest{}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,19 @@ type revokeCaptureSessions struct {
|
|||
closedRawAuthKeyIDs [][8]byte
|
||||
}
|
||||
|
||||
type expiringCaptureSessions struct {
|
||||
*captureSessions
|
||||
expiresAt int
|
||||
}
|
||||
|
||||
type metadataBlindSessions struct {
|
||||
SessionBinder
|
||||
}
|
||||
|
||||
func (s *expiringCaptureSessions) AuthKeyExpiresAtForSession([8]byte, int64) (int, bool) {
|
||||
return s.expiresAt, true
|
||||
}
|
||||
|
||||
func (s *revokeCaptureSessions) CloseSessionsForBusinessAuthKey(authKeyID [8]byte) int {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
|
@ -31,6 +44,76 @@ func (s *revokeCaptureSessions) CloseSessionsForRawAuthKeyExcept(authKeyID [8]by
|
|||
return 1
|
||||
}
|
||||
|
||||
func TestCachedRawTemporarySessionReResolvesDurableBinding(t *testing.T) {
|
||||
tempAuthKeyID := [8]byte{0x76, 0x76, 0x76, 0x76, 0x76, 0x76, 0x76, 0x76}
|
||||
permAuthKeyID := [8]byte{0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36}
|
||||
base := &captureSessions{}
|
||||
base.BindAuthKeyForSession(tempAuthKeyID, 554, tempAuthKeyID)
|
||||
sessions := &expiringCaptureSessions{
|
||||
captureSessions: base,
|
||||
expiresAt: int(time.Now().Add(time.Hour).Unix()),
|
||||
}
|
||||
auth := &captureAuthService{
|
||||
resolvedAuthKeyID: permAuthKeyID,
|
||||
hasResolved: true,
|
||||
userID: 1000000001,
|
||||
}
|
||||
r := New(Config{TempKeyResolveCacheTTL: time.Minute}, Deps{
|
||||
Auth: auth,
|
||||
Files: &fakeFiles{},
|
||||
Sessions: sessions,
|
||||
}, zaptest.NewLogger(t), clock.System)
|
||||
|
||||
var in bin.Buffer
|
||||
if err := (&tg.UploadSaveFilePartRequest{FileID: 19, FilePart: 0, Bytes: []byte{1}}).Encode(&in); err != nil {
|
||||
t.Fatalf("encode: %v", err)
|
||||
}
|
||||
if _, err := r.Dispatch(context.Background(), tempAuthKeyID, 554, &in); err != nil {
|
||||
t.Fatalf("dispatch: %v", err)
|
||||
}
|
||||
if auth.resolveCount != 1 {
|
||||
t.Fatalf("ResolveAuthKey calls = %d, want 1 for cached raw temporary session", auth.resolveCount)
|
||||
}
|
||||
got := sessions.snapshot()
|
||||
if got.authKeyID != permAuthKeyID || got.userID != 1000000001 {
|
||||
t.Fatalf("session = auth %x user %d, want perm/user", got.authKeyID, got.userID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCachedRawSessionWithoutMetadataFailsClosedToDurableResolver(t *testing.T) {
|
||||
tempAuthKeyID := [8]byte{0x75, 0x75, 0x75, 0x75, 0x75, 0x75, 0x75, 0x75}
|
||||
permAuthKeyID := [8]byte{0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35}
|
||||
// captureSessions intentionally has no RawAuthKeyMetadataProvider capability.
|
||||
// Missing metadata is not evidence that raw is permanent.
|
||||
base := &captureSessions{}
|
||||
base.BindAuthKeyForSession(tempAuthKeyID, 553, tempAuthKeyID)
|
||||
sessions := &metadataBlindSessions{SessionBinder: base}
|
||||
auth := &captureAuthService{
|
||||
resolvedAuthKeyID: permAuthKeyID,
|
||||
hasResolved: true,
|
||||
userID: 1000000001,
|
||||
}
|
||||
r := New(Config{TempKeyResolveCacheTTL: time.Minute}, Deps{
|
||||
Auth: auth,
|
||||
Files: &fakeFiles{},
|
||||
Sessions: sessions,
|
||||
}, zaptest.NewLogger(t), clock.System)
|
||||
|
||||
var in bin.Buffer
|
||||
if err := (&tg.UploadSaveFilePartRequest{FileID: 18, FilePart: 0, Bytes: []byte{1}}).Encode(&in); err != nil {
|
||||
t.Fatalf("encode: %v", err)
|
||||
}
|
||||
if _, err := r.Dispatch(context.Background(), tempAuthKeyID, 553, &in); err != nil {
|
||||
t.Fatalf("dispatch: %v", err)
|
||||
}
|
||||
if auth.resolveCount != 1 {
|
||||
t.Fatalf("ResolveAuthKey calls = %d, want 1 without metadata proof", auth.resolveCount)
|
||||
}
|
||||
if got := base.snapshot(); got.authKeyID != permAuthKeyID || got.userID != 1000000001 {
|
||||
t.Fatalf("session = auth %x user %d, want canonical perm/user", got.authKeyID, got.userID)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTempKeyResolveCacheHitsWithinTTL 验证:TempKeyResolveCacheTTL>0 时,同一 temp key 的连续
|
||||
// 请求在 TTL 内只解析一次(首帧走 !hasCached 解析 1 次、次帧 hasCached 解析并填缓存 1 次,之后命中
|
||||
// 缓存不再打 ResolveAuthKey)。固化「缓存生效」语义,与现有「TTL=0 每帧重校验」的安全测试互补。
|
||||
|
|
|
|||
|
|
@ -101,6 +101,12 @@ func (s *captureSessions) AuthKeyIDForSession([8]byte, int64) ([8]byte, bool) {
|
|||
return s.authKeyID, s.authKeyResolved
|
||||
}
|
||||
|
||||
// captureSessions models an ordinary permanent-key connection unless a test
|
||||
// wraps/overrides it with temporary-key metadata.
|
||||
func (s *captureSessions) AuthKeyExpiresAtForSession([8]byte, int64) (int, bool) {
|
||||
return 0, true
|
||||
}
|
||||
|
||||
func (s *captureSessions) BindUserForAuthKey(rawAuthKeyID [8]byte, sessionID, userID int64) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue