1082 lines
42 KiB
Go
1082 lines
42 KiB
Go
package rpc
|
||
|
||
import (
|
||
"context"
|
||
"crypto/hmac"
|
||
"crypto/rand"
|
||
"crypto/sha256"
|
||
"encoding/binary"
|
||
"errors"
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
"go.uber.org/zap"
|
||
|
||
"github.com/iamxvbaba/td/proto"
|
||
"github.com/iamxvbaba/td/tg"
|
||
|
||
"github.com/iamxvbaba/td/tlprofile"
|
||
"telesrv/internal/app/auth"
|
||
"telesrv/internal/branding"
|
||
"telesrv/internal/domain"
|
||
)
|
||
|
||
// devCodeLength 是开发固定验证码长度,写入 auth.sentCode 的 type.length。
|
||
const devCodeLength = 5
|
||
|
||
// registerAuth 注册 auth.* RPC handler。
|
||
func (r *Router) registerAuth(d *tlprofile.Dispatcher) {
|
||
registerRPC[*tg.AuthBindTempAuthKeyRequest](d, tlprofile.SemanticMethodAuthBindTempAuthKey, func(ctx context.Context, layerRequest *tg.AuthBindTempAuthKeyRequest) (any, error) {
|
||
return r.onAuthBindTempAuthKey(ctx, layerRequest)
|
||
})
|
||
registerRPC[*tg.AuthExportLoginTokenRequest](d, tlprofile.SemanticMethodAuthExportLoginToken, func(ctx context.Context, layerRequest *tg.AuthExportLoginTokenRequest) (any, error) {
|
||
return r.onAuthExportLoginToken(ctx, layerRequest)
|
||
})
|
||
registerRPC[*tg.AuthImportLoginTokenRequest](d, tlprofile.SemanticMethodAuthImportLoginToken, func(ctx context.Context, layerRequest *tg.AuthImportLoginTokenRequest) (any, error) {
|
||
return r.onAuthImportLoginToken(ctx, layerRequest.
|
||
Token)
|
||
})
|
||
registerRPC[*tg.AuthAcceptLoginTokenRequest](d, tlprofile.SemanticMethodAuthAcceptLoginToken, func(ctx context.Context, layerRequest *tg.AuthAcceptLoginTokenRequest) (any, error) {
|
||
return r.onAuthAcceptLoginToken(ctx, layerRequest.
|
||
Token)
|
||
})
|
||
registerRPC[*tg.AuthExportAuthorizationRequest](d, tlprofile.SemanticMethodAuthExportAuthorization, func(ctx context.Context, layerRequest *tg.AuthExportAuthorizationRequest) (any, error) {
|
||
return r.onAuthExportAuthorization(ctx, layerRequest)
|
||
})
|
||
registerRPC[*tg.AuthImportAuthorizationRequest](d, tlprofile.SemanticMethodAuthImportAuthorization, func(ctx context.Context, req *tg.AuthImportAuthorizationRequest) (any, error) {
|
||
return r.onAuthImportAuthorization(ctx, req)
|
||
})
|
||
registerRPC[*tg.AuthDropTempAuthKeysRequest](d, tlprofile.SemanticMethodAuthDropTempAuthKeys, func(ctx context.Context, layerRequest *tg.AuthDropTempAuthKeysRequest) (any, error) {
|
||
exceptauthkeys := layerRequest.
|
||
ExceptAuthKeys
|
||
_ = exceptauthkeys
|
||
|
||
return true, nil
|
||
})
|
||
registerRPC[*tg.AuthInitPasskeyLoginRequest](d, tlprofile.SemanticMethodAuthInitPasskeyLogin, func(ctx context.Context, layerRequest *tg.AuthInitPasskeyLoginRequest) (any, error) {
|
||
return r.onAuthInitPasskeyLogin(ctx, layerRequest)
|
||
})
|
||
registerRPC[*tg.AuthFinishPasskeyLoginRequest](d, tlprofile.SemanticMethodAuthFinishPasskeyLogin, func(ctx context.Context, layerRequest *tg.AuthFinishPasskeyLoginRequest) (any, error) {
|
||
return r.onAuthFinishPasskeyLogin(ctx, layerRequest)
|
||
})
|
||
registerRPC[*tg.AuthSendCodeRequest](d, tlprofile.SemanticMethodAuthSendCode, func(ctx context.Context, layerRequest *tg.AuthSendCodeRequest) (any, error) {
|
||
return r.onAuthSendCode(ctx, layerRequest)
|
||
})
|
||
registerRPC[*tg.AuthResendCodeRequest](d, tlprofile.SemanticMethodAuthResendCode, func(ctx context.Context, layerRequest *tg.AuthResendCodeRequest) (any, error) {
|
||
return r.onAuthResendCode(ctx, layerRequest)
|
||
})
|
||
registerRPC[*tg.AuthCancelCodeRequest](d, tlprofile.SemanticMethodAuthCancelCode, func(ctx context.Context, layerRequest *tg.AuthCancelCodeRequest) (any, error) {
|
||
return r.onAuthCancelCode(ctx, layerRequest)
|
||
})
|
||
registerRPC[*tg.AuthSignInRequest](d, tlprofile.SemanticMethodAuthSignIn, func(ctx context.Context, layerRequest *tg.AuthSignInRequest) (any, error) {
|
||
return r.onAuthSignIn(ctx, layerRequest)
|
||
})
|
||
registerRPC[*tg.AuthSignUpRequest](d, tlprofile.SemanticMethodAuthSignUp, func(ctx context.Context, layerRequest *tg.AuthSignUpRequest) (any, error) {
|
||
return r.onAuthSignUp(ctx, layerRequest)
|
||
})
|
||
registerRPC[*tg.AuthImportBotAuthorizationRequest](d, tlprofile.SemanticMethodAuthImportBotAuthorization, func(ctx context.Context, layerRequest *tg.AuthImportBotAuthorizationRequest) (any, error) {
|
||
return r.onAuthImportBotAuthorization(ctx, layerRequest)
|
||
})
|
||
registerRPC[*tg.AuthLogOutRequest](d, tlprofile.SemanticMethodAuthLogOut, func(ctx context.Context, layerRequest *tg.AuthLogOutRequest) (any, error) {
|
||
return r.onAuthLogOut(ctx)
|
||
})
|
||
registerRPC[*tg.AuthResetAuthorizationsRequest](d, tlprofile.SemanticMethodAuthResetAuthorizations, func(ctx context.Context, layerRequest *tg.AuthResetAuthorizationsRequest) (any, error) {
|
||
return r.onAuthResetAuthorizations(ctx)
|
||
})
|
||
registerRPC[*tg.AuthCheckPasswordRequest](d, tlprofile.SemanticMethodAuthCheckPassword, func(ctx context.Context, layerRequest *tg.AuthCheckPasswordRequest) (any, error) {
|
||
return r.onAuthCheckPassword(ctx, layerRequest.
|
||
Password)
|
||
})
|
||
registerRPC[*tg.AuthRequestPasswordRecoveryRequest](d, tlprofile.SemanticMethodAuthRequestPasswordRecovery, func(ctx context.Context, layerRequest *tg.AuthRequestPasswordRecoveryRequest) (any, error) {
|
||
return r.onAuthRequestPasswordRecovery(ctx)
|
||
})
|
||
registerRPC[*tg.AuthRecoverPasswordRequest](d, tlprofile.SemanticMethodAuthRecoverPassword, func(ctx context.Context, layerRequest *tg.AuthRecoverPasswordRequest) (any, error) {
|
||
return r.onAuthRecoverPassword(ctx, layerRequest)
|
||
})
|
||
registerRPC[*tg.AuthCheckRecoveryPasswordRequest](d, tlprofile.SemanticMethodAuthCheckRecoveryPassword, func(ctx context.Context, layerRequest *tg.AuthCheckRecoveryPasswordRequest) (
|
||
|
||
// onAuthBindTempAuthKey 记录 TDesktop 的 PFS temp→perm auth key 绑定。
|
||
any, error) {
|
||
return r.onAuthCheckRecoveryPassword(ctx, layerRequest.
|
||
Code)
|
||
})
|
||
registerRPC[*tg.AuthResetLoginEmailRequest](d, tlprofile.SemanticMethodAuthResetLoginEmail, func(ctx context.Context, layerRequest *tg.AuthResetLoginEmailRequest) (any, error) {
|
||
return r.onAuthResetLoginEmail(ctx, layerRequest)
|
||
})
|
||
}
|
||
|
||
// exportAuthTokenTTL is how long an auth.exportAuthorization token stays
|
||
// valid. Clients use it within seconds (open a second connection to fetch
|
||
// media/files believed to live on a different dc_id, then immediately call
|
||
// auth.importAuthorization), so a short window is plenty.
|
||
const exportAuthTokenTTL = 5 * time.Minute
|
||
|
||
// exportAuthSecret HMAC-signs auth.exportAuthorization tokens. It is
|
||
// process-lifetime random (not persisted): every dc_id this server hands out
|
||
// aliases the SAME physical process (see owpengram_servers single-server
|
||
// backend + memory "lenient DC key exchange"), so export and import always
|
||
// happen against the same running instance within the token's short TTL —
|
||
// a restart between the two would just make the client redo the export,
|
||
// same as if the token had expired.
|
||
var exportAuthSecret = func() []byte {
|
||
b := make([]byte, 32)
|
||
_, _ = rand.Read(b)
|
||
return b
|
||
}()
|
||
|
||
// signExportAuthToken binds userID+expiry into an opaque, tamper-evident
|
||
// token: 8 bytes user_id big-endian, 8 bytes expiry unix big-endian, 32 bytes
|
||
// HMAC-SHA256 tag over the first 16.
|
||
func signExportAuthToken(userID int64) []byte {
|
||
payload := make([]byte, 16, 48)
|
||
binary.BigEndian.PutUint64(payload[0:8], uint64(userID))
|
||
binary.BigEndian.PutUint64(payload[8:16], uint64(time.Now().Add(exportAuthTokenTTL).Unix()))
|
||
mac := hmac.New(sha256.New, exportAuthSecret)
|
||
mac.Write(payload)
|
||
return mac.Sum(payload)
|
||
}
|
||
|
||
// verifyExportAuthToken checks the HMAC tag and expiry and returns the
|
||
// embedded user_id.
|
||
func verifyExportAuthToken(token []byte) (int64, error) {
|
||
if len(token) != 48 {
|
||
return 0, errors.New("export auth token: bad length")
|
||
}
|
||
payload, tag := token[:16], token[16:]
|
||
mac := hmac.New(sha256.New, exportAuthSecret)
|
||
mac.Write(payload)
|
||
if !hmac.Equal(tag, mac.Sum(nil)) {
|
||
return 0, errors.New("export auth token: bad signature")
|
||
}
|
||
expiry := int64(binary.BigEndian.Uint64(payload[8:16]))
|
||
if time.Now().Unix() > expiry {
|
||
return 0, errors.New("export auth token: expired")
|
||
}
|
||
return int64(binary.BigEndian.Uint64(payload[0:8])), nil
|
||
}
|
||
|
||
// onAuthExportAuthorization issues a short-lived signed token proving the
|
||
// current connection's logged-in identity, so a second connection the client
|
||
// opens for what it believes is a different data-center (typically to fetch
|
||
// media/files) can prove the same user via auth.importAuthorization without
|
||
// repeating the phone/code login flow. Every dc_id this server advertises
|
||
// aliases the same physical backend (see Owpengram::ApplyServerToDcOptions /
|
||
// OwpengramServers.applyServerConfig on the clients), so req.DCID carries no
|
||
// real routing meaning here and is intentionally not validated.
|
||
func (r *Router) onAuthExportAuthorization(ctx context.Context, req *tg.AuthExportAuthorizationRequest) (*tg.AuthExportedAuthorization, error) {
|
||
userID, ok, err := r.currentUserID(ctx)
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
if !ok || userID == 0 {
|
||
return nil, dcIDInvalidErr()
|
||
}
|
||
return &tg.AuthExportedAuthorization{
|
||
ID: userID,
|
||
Bytes: signExportAuthToken(userID),
|
||
}, nil
|
||
}
|
||
|
||
// onAuthImportAuthorization is the counterpart consumed on the second
|
||
// connection: verifies the token minted by onAuthExportAuthorization and, on
|
||
// success, binds THIS connection's auth_key/session to that user — the same
|
||
// effect a normal auth.signIn has once it succeeds.
|
||
func (r *Router) onAuthImportAuthorization(ctx context.Context, req *tg.AuthImportAuthorizationRequest) (tg.AuthAuthorizationClass, error) {
|
||
userID, err := verifyExportAuthToken(req.Bytes)
|
||
if err != nil || userID == 0 || userID != req.ID {
|
||
return nil, dcIDInvalidErr()
|
||
}
|
||
u, err := r.deps.Users.Self(ctx, userID)
|
||
if err != nil {
|
||
return nil, dcIDInvalidErr()
|
||
}
|
||
if authKeyID, ok := AuthKeyIDFrom(ctx); ok {
|
||
r.setAuthUserCache(authKeyID, userID, true)
|
||
}
|
||
r.bindSessionUser(ctx, userID)
|
||
return &tg.AuthAuthorization{User: r.tgSelfUser(u)}, nil
|
||
}
|
||
|
||
func (r *Router) onAuthBindTempAuthKey(ctx context.Context, req *tg.AuthBindTempAuthKeyRequest) (bool, error) {
|
||
if !layerRPCProfileEvidenceFresh(ctx) {
|
||
// The inner request is outside MTProto's mutable msg_id window. It may be
|
||
// decoded request-locally, but acknowledging it would mutate the durable
|
||
// temp→perm identity and every live session from stale replay evidence.
|
||
return false, bindTempAuthKeyErr(auth.ErrTempAuthKeyEmpty)
|
||
}
|
||
if r.deps.Auth == nil {
|
||
return true, nil
|
||
}
|
||
id, _ := RawAuthKeyIDFrom(ctx)
|
||
if id == ([8]byte{}) {
|
||
id, _ = AuthKeyIDFrom(ctx)
|
||
}
|
||
sessionID, _ := SessionIDFrom(ctx)
|
||
if err := r.deps.Auth.BindTempAuthKey(ctx, sessionID, domain.TempAuthKeyBinding{
|
||
TempAuthKeyID: id,
|
||
PermAuthKeyID: req.PermAuthKeyID,
|
||
Nonce: req.Nonce,
|
||
ExpiresAt: req.ExpiresAt,
|
||
EncryptedMessage: append([]byte(nil), req.EncryptedMessage...),
|
||
}); err != nil {
|
||
return false, bindTempAuthKeyErr(err)
|
||
}
|
||
permID := authKeyIDFromInt64(req.PermAuthKeyID)
|
||
// temp key (re)bind 后立即作废其 temp→perm 解析缓存,确保下一帧按新绑定重新解析,
|
||
// 不被 TTL 内的旧 perm 缓存命中(防跨账号串号)。
|
||
if id != ([8]byte{}) {
|
||
r.tempKeyResolveCache.Delete(id)
|
||
}
|
||
// Save atomically merged raw/permanent Layer observations. Both identities
|
||
// must now re-read that durable permanent primary; pre-bind process caches
|
||
// are not ordering evidence and cannot overwrite the transaction's winner.
|
||
r.invalidateAuthUserCache(id)
|
||
r.invalidateAuthUserCache(permID)
|
||
unlockLayerCommit := r.lockAuthLayerCommit(id, permID)
|
||
defer unlockLayerCommit()
|
||
r.invalidateBoundAuthKeyLayerResolution(id, permID)
|
||
if r.deps.Sessions != nil {
|
||
if all, ok := r.deps.Sessions.(RawAuthKeySessionBinder); ok {
|
||
all.BindAuthKeyForRawAuthKey(id, permID)
|
||
} else {
|
||
r.deps.Sessions.BindAuthKeyForSession(id, sessionID, permID)
|
||
}
|
||
}
|
||
layer, _, err := r.resolveAuthKeyLayerDefault(ctx, permID)
|
||
if err != nil {
|
||
if clearer, ok := r.deps.Sessions.(AuthKeyInheritedLayerClearer); ok {
|
||
clearer.ClearInheritedLayerForRawAuthKey(id)
|
||
}
|
||
if r.log != nil {
|
||
r.log.Warn("reload merged permanent layer after temp auth key bind failed",
|
||
zap.String("raw_auth_key_id", fmt.Sprintf("%x", id[:])),
|
||
zap.String("perm_auth_key_id", fmt.Sprintf("%x", permID[:])),
|
||
zap.Error(err))
|
||
}
|
||
return false, internalErr()
|
||
}
|
||
r.cacheBoundAuthKeyLayerResolution(id, permID)
|
||
if isSupportedLayer(layer) {
|
||
if refresher, ok := r.deps.Sessions.(AuthKeyLayerRefresher); ok {
|
||
refresher.RefreshInheritedLayerForRawAuthKey(id, layer)
|
||
} else if binder, ok := r.deps.Sessions.(AuthKeyLayerBinder); ok {
|
||
binder.SeedInheritedLayerForRawAuthKey(id, layer)
|
||
}
|
||
} else if clearer, ok := r.deps.Sessions.(AuthKeyInheritedLayerClearer); ok {
|
||
clearer.ClearInheritedLayerForRawAuthKey(id)
|
||
}
|
||
return true, nil
|
||
}
|
||
|
||
func (r *Router) invalidateBoundAuthKeyLayerResolution(authKeyIDs ...[8]byte) {
|
||
r.clientInfoMu.Lock()
|
||
defer r.clientInfoMu.Unlock()
|
||
for _, authKeyID := range authKeyIDs {
|
||
if info, ok := r.authInfo[authKeyID]; ok {
|
||
info.layer = 0
|
||
info.layerObservationID = 0
|
||
info.layerAdmissionSeq = 0
|
||
info.authKeyInfoChecked = false
|
||
info.authorizationChecked = false
|
||
info.layerBlocked = false
|
||
info.layerBlockedByAuthKey = false
|
||
r.authInfo[authKeyID] = info
|
||
}
|
||
}
|
||
}
|
||
|
||
func (r *Router) cacheBoundAuthKeyLayerResolution(rawAuthKeyID, permAuthKeyID [8]byte) {
|
||
r.clientInfoMu.Lock()
|
||
defer r.clientInfoMu.Unlock()
|
||
if r.authInfo == nil {
|
||
r.authInfo = make(map[[8]byte]clientSessionInfo)
|
||
}
|
||
if _, exists := r.authInfo[rawAuthKeyID]; !exists {
|
||
evictMapEntryIfFullLocked(r.authInfo, maxAuthInfoEntries)
|
||
}
|
||
canonical := r.authInfo[permAuthKeyID]
|
||
info := r.authInfo[rawAuthKeyID]
|
||
// The bind transaction made the permanent row authoritative for both
|
||
// identities. Copy its complete resolution tuple: a Layer without the same
|
||
// observation token (or a stale blocked bit) would let later cache merging
|
||
// manufacture an ordering state that never existed durably.
|
||
info.layer = canonical.layer
|
||
info.layerObservationID = canonical.layerObservationID
|
||
info.layerAdmissionSeq = canonical.layerAdmissionSeq
|
||
info.layerBlocked = canonical.layerBlocked
|
||
info.layerBlockedByAuthKey = canonical.layerBlockedByAuthKey
|
||
info.authKeyInfoChecked = canonical.authKeyInfoChecked
|
||
info.authorizationChecked = canonical.authorizationChecked
|
||
r.authInfo[rawAuthKeyID] = info
|
||
}
|
||
|
||
// onAuthExportLoginToken 给 QR 登录请求方返回短期 token;扫码端接受后,同一目标
|
||
// session 后续 export 会升级为 auth.loginTokenSuccess。
|
||
func (r *Router) onAuthExportLoginToken(ctx context.Context, req *tg.AuthExportLoginTokenRequest) (tg.AuthLoginTokenClass, error) {
|
||
target, ok := loginTokenTargetFromContext(ctx)
|
||
if !ok {
|
||
return nil, internalErr()
|
||
}
|
||
authz := r.authzFromCtx(ctx)
|
||
result, err := r.loginTokens.export(r.clock.Now(), target, authz, req.ExceptIDs)
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
if result.accepted {
|
||
return r.authLoginTokenSuccess(ctx, result.acceptedAuth)
|
||
}
|
||
return &tg.AuthLoginToken{Expires: int(result.expires.Unix()), Token: result.token}, nil
|
||
}
|
||
|
||
func (r *Router) onAuthImportLoginToken(ctx context.Context, token []byte) (tg.AuthLoginTokenClass, error) {
|
||
result, err := r.loginTokens.lookup(r.clock.Now(), token)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if result.accepted {
|
||
return r.authLoginTokenSuccess(ctx, result.acceptedAuth)
|
||
}
|
||
return &tg.AuthLoginToken{Expires: int(result.expires.Unix()), Token: result.token}, nil
|
||
}
|
||
|
||
func (r *Router) onAuthAcceptLoginToken(ctx context.Context, token []byte) (*tg.Authorization, error) {
|
||
userID, ok, err := r.currentUserID(ctx)
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
if !ok || userID == 0 {
|
||
return nil, authKeyUnregisteredErr()
|
||
}
|
||
if r.deps.Auth == nil {
|
||
return nil, internalErr()
|
||
}
|
||
now := r.clock.Now()
|
||
accept, err := r.loginTokens.beginAccept(now, token, userID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
scannerAuthKeyID, _ := AuthKeyIDFrom(ctx)
|
||
if scannerAuthKeyID != ([8]byte{}) && scannerAuthKeyID == accept.authz.AuthKeyID {
|
||
r.loginTokens.failAccept(token)
|
||
return nil, authTokenExceptionErr()
|
||
}
|
||
authz := accept.authz
|
||
authz.UserID = userID
|
||
authz.PasswordPending = false
|
||
if err := r.clearAuthKeyState(ctx, authz.AuthKeyID); err != nil {
|
||
r.loginTokens.failAccept(token)
|
||
return nil, internalErr()
|
||
}
|
||
bound, err := r.deps.Auth.AcceptLoginToken(ctx, authz, userID)
|
||
if err != nil {
|
||
r.loginTokens.failAccept(token)
|
||
if errors.Is(err, auth.ErrSystemUserLoginForbidden) {
|
||
return nil, authKeyUnregisteredErr()
|
||
}
|
||
return nil, internalErr()
|
||
}
|
||
if bound.UserID == 0 {
|
||
bound.UserID = userID
|
||
}
|
||
r.loginTokens.finishAccept(now, token, userID, bound)
|
||
r.invalidateAuthUserCache(bound.AuthKeyID)
|
||
r.setAuthUserCache(bound.AuthKeyID, userID, true)
|
||
r.bindLoginTokenTarget(accept.target, userID)
|
||
r.pushLoginTokenAccepted(ctx, accept.target)
|
||
out := tgAuthorization(bound, scannerAuthKeyID, int(now.Unix()))
|
||
return &out, nil
|
||
}
|
||
|
||
func loginTokenTargetFromContext(ctx context.Context) (loginTokenTarget, bool) {
|
||
authKeyID, _ := AuthKeyIDFrom(ctx)
|
||
rawAuthKeyID, _ := RawAuthKeyIDFrom(ctx)
|
||
if rawAuthKeyID == ([8]byte{}) {
|
||
rawAuthKeyID = authKeyID
|
||
}
|
||
sessionID, _ := SessionIDFrom(ctx)
|
||
return loginTokenTarget{rawAuthKeyID: rawAuthKeyID, authKeyID: authKeyID, sessionID: sessionID}, true
|
||
}
|
||
|
||
func (r *Router) authLoginTokenSuccess(ctx context.Context, a domain.Authorization) (tg.AuthLoginTokenClass, error) {
|
||
if r.deps.Users == nil || a.UserID == 0 {
|
||
return nil, internalErr()
|
||
}
|
||
u, err := r.deps.Users.Self(ctx, a.UserID)
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
return &tg.AuthLoginTokenSuccess{
|
||
Authorization: &tg.AuthAuthorization{User: r.tgSelfUser(u)},
|
||
}, nil
|
||
}
|
||
|
||
func (r *Router) bindLoginTokenTarget(target loginTokenTarget, userID int64) {
|
||
if r.deps.Sessions == nil || target.sessionID == 0 {
|
||
return
|
||
}
|
||
r.deps.Sessions.BindAuthKeyForSession(target.rawAuthKeyID, target.sessionID, target.authKeyID)
|
||
r.deps.Sessions.BindUserForAuthKey(target.rawAuthKeyID, target.sessionID, userID)
|
||
r.announceSessionOnline(loginTokenTargetContext(target, userID), userID)
|
||
}
|
||
|
||
func loginTokenTargetContext(target loginTokenTarget, userID int64) context.Context {
|
||
ctx := context.Background()
|
||
ctx = WithRawAuthKeyID(ctx, target.rawAuthKeyID)
|
||
ctx = WithAuthKeyID(ctx, target.authKeyID)
|
||
ctx = WithSessionID(ctx, target.sessionID)
|
||
ctx = WithUserID(ctx, userID)
|
||
return ctx
|
||
}
|
||
|
||
func (r *Router) pushLoginTokenAccepted(ctx context.Context, target loginTokenTarget) {
|
||
if r.deps.Sessions == nil || target.sessionID == 0 {
|
||
return
|
||
}
|
||
updates := &tg.UpdateShort{
|
||
Update: &tg.UpdateLoginToken{},
|
||
Date: int(r.clock.Now().Unix()),
|
||
}
|
||
if immediate, ok := r.deps.Sessions.(ImmediateSessionPusher); ok {
|
||
if err := immediate.PushToSessionForAuthKeyImmediate(ctx, target.rawAuthKeyID, target.sessionID, proto.MessageFromServer, updates); err != nil {
|
||
r.log.Debug("push login token accepted immediate", zap.Int64("session_id", target.sessionID), zap.Error(err))
|
||
}
|
||
return
|
||
}
|
||
if err := r.deps.Sessions.PushToSessionForAuthKey(ctx, target.rawAuthKeyID, target.sessionID, proto.MessageFromServer, updates); err != nil {
|
||
r.log.Debug("push login token accepted", zap.Int64("session_id", target.sessionID), zap.Error(err))
|
||
}
|
||
}
|
||
|
||
// onAuthSendCode 处理 auth.sendCode:生成 phone_code_hash 并返回 sentCode。
|
||
// 若该手机号账号设置了登录邮箱,验证码改投递到邮箱,返回 sentCodeTypeEmailCode
|
||
// (客户端据此进入"输入邮箱验证码"界面,随后用 auth.signIn 的 email_verification 完成登录)。
|
||
func (r *Router) onAuthSendCode(ctx context.Context, req *tg.AuthSendCodeRequest) (tg.AuthSentCodeClass, error) {
|
||
if err := r.checkAuthCodeRateLimit(ctx, req.PhoneNumber); err != nil {
|
||
return nil, err
|
||
}
|
||
r.rememberClientAPIID(ctx, req.APIID)
|
||
if r.log != nil {
|
||
normalized := domain.NormalizePhone(req.PhoneNumber)
|
||
_, decodeOK := domain.DecodeEmailPhone(normalized)
|
||
r.log.Info("auth.sendCode phone diagnostics",
|
||
zap.Int("phone_len", len(normalized)),
|
||
zap.Bool("looks_like_email_signup", domain.IsEmailSignupPhone(normalized)),
|
||
zap.Bool("decodes_to_email", decodeOK))
|
||
}
|
||
hash, err := r.deps.Auth.SendCode(ctx, req.PhoneNumber)
|
||
if err != nil {
|
||
if errors.Is(err, auth.ErrPhoneNumberInvalid) ||
|
||
errors.Is(err, auth.ErrSystemUserLoginForbidden) {
|
||
return nil, phoneNumberInvalidErr()
|
||
}
|
||
return nil, internalErr()
|
||
}
|
||
return r.tgSentCodeForHash(ctx, hash)
|
||
}
|
||
|
||
func tgSentCode(hash string) tg.AuthSentCodeClass {
|
||
return tgSentCodeWithLength(hash, devCodeLength)
|
||
}
|
||
|
||
func tgSentCodeWithLength(hash string, length int) tg.AuthSentCodeClass {
|
||
if length <= 0 {
|
||
length = devCodeLength
|
||
}
|
||
return &tg.AuthSentCode{
|
||
Type: &tg.AuthSentCodeTypeApp{Length: length},
|
||
PhoneCodeHash: hash,
|
||
}
|
||
}
|
||
|
||
func tgSMSSentCode(hash string, length int) tg.AuthSentCodeClass {
|
||
if length <= 0 {
|
||
length = devCodeLength
|
||
}
|
||
return &tg.AuthSentCode{
|
||
Type: &tg.AuthSentCodeTypeSMS{Length: length},
|
||
PhoneCodeHash: hash,
|
||
}
|
||
}
|
||
|
||
func tgEmailSentCode(hash, emailPattern string, length int) tg.AuthSentCodeClass {
|
||
if length <= 0 {
|
||
length = devCodeLength
|
||
}
|
||
codeType := &tg.AuthSentCodeTypeEmailCode{
|
||
EmailPattern: emailPattern,
|
||
Length: length,
|
||
}
|
||
// reset_available_period=0 表示可立即调用 auth.resetLoginEmail(开发环境无等待期),
|
||
// 让客户端的"无法访问邮箱?"逃生入口可用。
|
||
codeType.SetResetAvailablePeriod(0)
|
||
return &tg.AuthSentCode{
|
||
Type: codeType,
|
||
PhoneCodeHash: hash,
|
||
}
|
||
}
|
||
|
||
func tgEmailSetupRequiredSentCode(hash string) tg.AuthSentCodeClass {
|
||
return &tg.AuthSentCode{
|
||
Type: &tg.AuthSentCodeTypeSetUpEmailRequired{},
|
||
PhoneCodeHash: hash,
|
||
}
|
||
}
|
||
|
||
func (r *Router) tgSentCodeForHash(ctx context.Context, hash string) (tg.AuthSentCodeClass, error) {
|
||
if r.deps.Auth == nil {
|
||
return tgSentCode(hash), nil
|
||
}
|
||
delivery, found, err := r.deps.Auth.CodeDelivery(ctx, hash)
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
if !found {
|
||
return nil, signInErr(auth.ErrCodeExpired)
|
||
}
|
||
switch delivery.Kind {
|
||
case domain.AuthCodeDeliverySMS:
|
||
return tgSMSSentCode(hash, delivery.Length), nil
|
||
case domain.AuthCodeDeliveryEmail:
|
||
return tgEmailSentCode(hash, delivery.EmailPattern, delivery.Length), nil
|
||
case domain.AuthCodeDeliveryEmailSetupRequired:
|
||
return tgEmailSetupRequiredSentCode(hash), nil
|
||
default:
|
||
return tgSentCodeWithLength(hash, delivery.Length), nil
|
||
}
|
||
}
|
||
|
||
// onAuthSignIn 处理 auth.signIn:校验验证码;用户不存在时返回 SignUpRequired。
|
||
// 带 email_verification 时走登录邮箱路径(验证码来自邮箱而非短信)。
|
||
func (r *Router) onAuthSignIn(ctx context.Context, req *tg.AuthSignInRequest) (tg.AuthAuthorizationClass, error) {
|
||
var (
|
||
u domain.User
|
||
needSignUp bool
|
||
err error
|
||
)
|
||
if verification, ok := req.GetEmailVerification(); ok {
|
||
u, _, needSignUp, err = r.deps.Auth.SignInWithEmail(ctx, r.authzFromCtx(ctx), req.PhoneNumber, req.PhoneCodeHash, emailVerificationCode(verification))
|
||
} else {
|
||
u, _, needSignUp, err = r.deps.Auth.SignIn(ctx, r.authzFromCtx(ctx), req.PhoneNumber, req.PhoneCodeHash, req.PhoneCode)
|
||
}
|
||
return r.finishAuthSignIn(ctx, u, needSignUp, err)
|
||
}
|
||
|
||
func (r *Router) finishAuthSignIn(ctx context.Context, u domain.User, needSignUp bool, err error) (tg.AuthAuthorizationClass, error) {
|
||
if err != nil {
|
||
if errors.Is(err, domain.ErrSessionPasswordNeeded) && u.ID != 0 {
|
||
// 两步验证未完成:绝不能把 auth_key/session 标记为已登录,否则客户端忽略
|
||
// SESSION_PASSWORD_NEEDED、直接调用业务 RPC 即可绕过 2FA。失效缓存并把 session
|
||
// 置为未授权,让后续鉴权重新读到 password_pending 并拒绝;待 checkPassword 通过后再授权。
|
||
if id, ok := AuthKeyIDFrom(ctx); ok {
|
||
r.invalidateAuthUserCache(id)
|
||
}
|
||
r.bindSessionUser(ctx, 0)
|
||
}
|
||
return nil, signInErr(err)
|
||
}
|
||
if needSignUp {
|
||
return &tg.AuthAuthorizationSignUpRequired{}, nil
|
||
}
|
||
if id, ok := AuthKeyIDFrom(ctx); ok {
|
||
r.setAuthUserCache(id, u.ID, true)
|
||
}
|
||
r.bindSessionUser(ctx, u.ID)
|
||
r.pushSignInServiceNotificationToOthers(ctx, u)
|
||
return &tg.AuthAuthorization{User: r.tgSelfUser(u)}, nil
|
||
}
|
||
|
||
func (r *Router) onAuthResendCode(ctx context.Context, req *tg.AuthResendCodeRequest) (tg.AuthSentCodeClass, error) {
|
||
if err := r.checkAuthCodeRateLimit(ctx, req.PhoneNumber); err != nil {
|
||
return nil, err
|
||
}
|
||
if userID, authorized, err := r.currentUserID(ctx); err == nil && authorized && userID != 0 {
|
||
if svc, ok := r.deps.Account.(accountDeletionService); ok {
|
||
authKeyID, _ := AuthKeyIDFrom(ctx)
|
||
sessionID, _ := SessionIDFrom(ctx)
|
||
hash, delivery, handled, err := svc.ResendConfirmPhoneCode(ctx, userID, authKeyID, sessionID, req.PhoneNumber, req.PhoneCodeHash)
|
||
if handled {
|
||
if err != nil {
|
||
return nil, accountDeletionErr(err)
|
||
}
|
||
return tgSMSSentCode(hash, delivery.Length), nil
|
||
}
|
||
}
|
||
}
|
||
var hash string
|
||
var err error
|
||
if scoped, ok := r.deps.Auth.(interface {
|
||
ResendCodeForAuthKey(context.Context, [8]byte, string, string) (string, error)
|
||
}); ok {
|
||
authKeyID, _ := AuthKeyIDFrom(ctx)
|
||
hash, err = scoped.ResendCodeForAuthKey(ctx, authKeyID, req.PhoneNumber, req.PhoneCodeHash)
|
||
} else {
|
||
hash, err = r.deps.Auth.ResendCode(ctx, req.PhoneNumber, req.PhoneCodeHash)
|
||
}
|
||
if err != nil {
|
||
return nil, signInErr(err)
|
||
}
|
||
return r.tgSentCodeForHash(ctx, hash)
|
||
}
|
||
|
||
func (r *Router) onAuthCancelCode(ctx context.Context, req *tg.AuthCancelCodeRequest) (bool, error) {
|
||
if userID, authorized, err := r.currentUserID(ctx); err == nil && authorized && userID != 0 {
|
||
if svc, ok := r.deps.Account.(accountDeletionService); ok {
|
||
authKeyID, _ := AuthKeyIDFrom(ctx)
|
||
handled, err := svc.CancelConfirmPhoneCode(ctx, userID, authKeyID, req.PhoneNumber, req.PhoneCodeHash)
|
||
if handled {
|
||
if err != nil {
|
||
return false, accountDeletionErr(err)
|
||
}
|
||
return true, nil
|
||
}
|
||
}
|
||
}
|
||
var err error
|
||
if scoped, ok := r.deps.Auth.(interface {
|
||
CancelCodeForAuthKey(context.Context, [8]byte, string, string) error
|
||
}); ok {
|
||
authKeyID, _ := AuthKeyIDFrom(ctx)
|
||
err = scoped.CancelCodeForAuthKey(ctx, authKeyID, req.PhoneNumber, req.PhoneCodeHash)
|
||
} else {
|
||
err = r.deps.Auth.CancelCode(ctx, req.PhoneNumber, req.PhoneCodeHash)
|
||
}
|
||
if err != nil {
|
||
return false, signInErr(err)
|
||
}
|
||
return true, nil
|
||
}
|
||
|
||
func (r *Router) onAuthResetAuthorizations(ctx context.Context) (bool, error) {
|
||
userID, _, err := r.currentUserID(ctx)
|
||
if err != nil {
|
||
return false, internalErr()
|
||
}
|
||
authKeyID, _ := AuthKeyIDFrom(ctx)
|
||
deleted, err := r.deps.Auth.ResetAuthorizations(ctx, userID, authKeyID)
|
||
if err != nil {
|
||
return false, internalErr()
|
||
}
|
||
for _, a := range deleted {
|
||
r.revokeAuthKeySessions(a.AuthKeyID)
|
||
_ = r.clearAuthKeyState(ctx, a.AuthKeyID)
|
||
// P1 修复:撤销其它会话同样销毁其 auth_key,级联 discard 该设备绑定的活跃密聊并通知对端。
|
||
r.discardSecretChatsForAuthKey(ctx, businessAuthKeyInt64(a.AuthKeyID), userID)
|
||
}
|
||
return true, nil
|
||
}
|
||
|
||
func (r *Router) onAuthCheckPassword(ctx context.Context, password tg.InputCheckPasswordSRPClass) (tg.AuthAuthorizationClass, error) {
|
||
authKeyID, _ := AuthKeyIDFrom(ctx)
|
||
userID, authorized, pending, err := r.currentOrPendingPasswordUserID(ctx)
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
if !pending && (!authorized || userID == 0) {
|
||
return nil, passwordHashInvalidErr()
|
||
}
|
||
if r.deps.Account == nil {
|
||
return nil, passwordHashInvalidErr()
|
||
}
|
||
if err := r.deps.Account.CheckPassword(ctx, userID, domainPasswordCheck(password)); err != nil {
|
||
return nil, passwordErr(err)
|
||
}
|
||
// 两步验证通过:清除 password_pending 并把 auth_key/session 提升为完全授权。
|
||
if pending {
|
||
if err := r.completePendingPasswordSignIn(ctx, authKeyID, userID); err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
}
|
||
u, err := r.deps.Users.Self(ctx, userID)
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
return &tg.AuthAuthorization{User: r.tgSelfUser(u)}, nil
|
||
}
|
||
|
||
func (r *Router) onAuthRequestPasswordRecovery(ctx context.Context) (*tg.AuthPasswordRecovery, error) {
|
||
userID, _, _, err := r.currentOrPendingPasswordUserID(ctx)
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
pattern, err := r.deps.Account.RequestPasswordRecovery(ctx, userID)
|
||
if err != nil {
|
||
return nil, passwordErr(err)
|
||
}
|
||
return &tg.AuthPasswordRecovery{EmailPattern: pattern}, nil
|
||
}
|
||
|
||
func (r *Router) onAuthRecoverPassword(ctx context.Context, req *tg.AuthRecoverPasswordRequest) (tg.AuthAuthorizationClass, error) {
|
||
authKeyID, _ := AuthKeyIDFrom(ctx)
|
||
userID, _, pending, err := r.currentOrPendingPasswordUserID(ctx)
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
var input *domain.PasswordInputSettings
|
||
if settings, ok := req.GetNewSettings(); ok {
|
||
converted, err := domainPasswordInputSettings(settings)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
input = &converted
|
||
}
|
||
if err := r.deps.Account.RecoverPassword(ctx, userID, req.Code, input); err != nil {
|
||
return nil, passwordErr(err)
|
||
}
|
||
if pending {
|
||
if err := r.completePendingPasswordSignIn(ctx, authKeyID, userID); err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
}
|
||
u, err := r.deps.Users.Self(ctx, userID)
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
return &tg.AuthAuthorization{User: r.tgSelfUser(u)}, nil
|
||
}
|
||
|
||
func (r *Router) onAuthCheckRecoveryPassword(ctx context.Context, code string) (bool, error) {
|
||
userID, _, _, err := r.currentOrPendingPasswordUserID(ctx)
|
||
if err != nil {
|
||
return false, internalErr()
|
||
}
|
||
if err := r.deps.Account.CheckRecoveryPassword(ctx, userID, code); err != nil {
|
||
return false, passwordErr(err)
|
||
}
|
||
return true, nil
|
||
}
|
||
|
||
// currentOrPendingPasswordUserID returns the fully authorized user when present;
|
||
// otherwise it allows the narrow 2FA login continuation path to locate the user
|
||
// attached to a password_pending auth key. The pending identity must not be used
|
||
// by general business RPCs.
|
||
func (r *Router) currentOrPendingPasswordUserID(ctx context.Context) (userID int64, authorized bool, passwordPending bool, err error) {
|
||
userID, authorized, err = r.currentUserID(ctx)
|
||
if err != nil || authorized {
|
||
return userID, authorized, false, err
|
||
}
|
||
if r.deps.Auth == nil {
|
||
return userID, authorized, false, nil
|
||
}
|
||
authKeyID, ok := AuthKeyIDFrom(ctx)
|
||
if !ok {
|
||
return userID, authorized, false, nil
|
||
}
|
||
pendingUserID, pending, err := r.deps.Auth.PendingPasswordUserID(ctx, authKeyID)
|
||
if err != nil {
|
||
return 0, false, false, err
|
||
}
|
||
if !pending || pendingUserID == 0 {
|
||
return userID, authorized, false, nil
|
||
}
|
||
return pendingUserID, false, true, nil
|
||
}
|
||
|
||
func (r *Router) completePendingPasswordSignIn(ctx context.Context, authKeyID [8]byte, userID int64) error {
|
||
if r.deps.Auth == nil {
|
||
return nil
|
||
}
|
||
if err := r.deps.Auth.CompletePasswordSignIn(ctx, authKeyID); err != nil {
|
||
return err
|
||
}
|
||
r.invalidateAuthUserCache(authKeyID)
|
||
r.setAuthUserCache(authKeyID, userID, true)
|
||
r.bindSessionUser(ctx, userID)
|
||
return nil
|
||
}
|
||
|
||
// onAuthResetLoginEmail 处理 auth.resetLoginEmail:用户登录设备时无法访问登录邮箱时
|
||
// 清除登录邮箱,改回手机验证码登录,返回一个新的手机 sentCode 供其继续。
|
||
type loginEmailResetConsumer interface {
|
||
ConsumeLoginEmailReset(ctx context.Context, phone, phoneCodeHash string) (userID int64, err error)
|
||
SendPhoneCodeAfterLoginEmailReset(ctx context.Context, phone string, expectedUserID int64) (string, error)
|
||
}
|
||
|
||
func (r *Router) onAuthResetLoginEmail(ctx context.Context, req *tg.AuthResetLoginEmailRequest) (tg.AuthSentCodeClass, error) {
|
||
if r.deps.Account == nil || r.deps.Auth == nil {
|
||
return nil, internalErr()
|
||
}
|
||
if err := r.checkAuthCodeRateLimit(ctx, req.PhoneNumber); err != nil {
|
||
return nil, err
|
||
}
|
||
resetConsumer, ok := r.deps.Auth.(loginEmailResetConsumer)
|
||
if !ok {
|
||
return nil, internalErr()
|
||
}
|
||
resetUserID, err := resetConsumer.ConsumeLoginEmailReset(ctx, req.PhoneNumber, req.PhoneCodeHash)
|
||
if err != nil {
|
||
return nil, signInErr(err)
|
||
}
|
||
if err := r.deps.Account.ClearLoginEmail(ctx, resetUserID); err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
hash, err := resetConsumer.SendPhoneCodeAfterLoginEmailReset(ctx, req.PhoneNumber, resetUserID)
|
||
if err != nil {
|
||
if errors.Is(err, auth.ErrCodeExpired) || errors.Is(err, auth.ErrCodeInvalid) {
|
||
return nil, signInErr(err)
|
||
}
|
||
if errors.Is(err, auth.ErrPhoneNumberInvalid) ||
|
||
errors.Is(err, auth.ErrSystemUserLoginForbidden) {
|
||
return nil, phoneNumberInvalidErr()
|
||
}
|
||
return nil, internalErr()
|
||
}
|
||
return r.tgSentCodeForHash(ctx, hash)
|
||
}
|
||
|
||
// emailVerificationCode 从 emailVerification 取出可校验的字符串(验证码 / Google·Apple
|
||
// 令牌);最终必须由 auth service 对签发记录精确校验。
|
||
// onAuthInitPasskeyLogin 处理 auth.initPasskeyLogin:生成一次性断言挑战(discoverable),
|
||
// 以 DataJSON(顶层含 publicKey)返回。免授权(登录前)。
|
||
func (r *Router) onAuthInitPasskeyLogin(ctx context.Context, req *tg.AuthInitPasskeyLoginRequest) (*tg.AuthPasskeyLoginOptions, error) {
|
||
if r.deps.Passkey == nil {
|
||
return &tg.AuthPasskeyLoginOptions{Options: tg.DataJSON{Data: "{}"}}, nil
|
||
}
|
||
options, err := r.deps.Passkey.InitLogin(ctx)
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
return &tg.AuthPasskeyLoginOptions{Options: tg.DataJSON{Data: string(options)}}, nil
|
||
}
|
||
|
||
// onAuthFinishPasskeyLogin 处理 auth.finishPasskeyLogin:验证登录断言并绑定 auth_key。
|
||
// 收尾与 signIn 同构(Bind 原子切换 update baseline → 授权缓存 → session 绑定);
|
||
// passkey 是强因子,直接完全授权
|
||
// (不走 SESSION_PASSWORD_NEEDED)。FromDCID/FromAuthKeyID 为多 DC 重路由用,本单 DC 忽略。
|
||
func (r *Router) onAuthFinishPasskeyLogin(ctx context.Context, req *tg.AuthFinishPasskeyLoginRequest) (tg.AuthAuthorizationClass, error) {
|
||
if r.deps.Passkey == nil || r.deps.Auth == nil {
|
||
return nil, internalErr()
|
||
}
|
||
credID, login, ok := passkeyLoginFromCredential(req.Credential)
|
||
if !ok {
|
||
return nil, passkeyErr(domain.ErrPasskeyInvalid)
|
||
}
|
||
userID, err := r.deps.Passkey.FinishLogin(ctx, credID, []byte(login.ClientData.Data), login.AuthenticatorData, login.Signature, login.UserHandle)
|
||
if err != nil {
|
||
return nil, passkeyErr(err)
|
||
}
|
||
u, err := r.deps.Auth.BindVerifiedLogin(ctx, r.authzFromCtx(ctx), userID)
|
||
if err != nil {
|
||
return nil, passkeyErr(err)
|
||
}
|
||
if id, ok := AuthKeyIDFrom(ctx); ok {
|
||
r.setAuthUserCache(id, u.ID, true)
|
||
}
|
||
r.bindSessionUser(ctx, u.ID)
|
||
return &tg.AuthAuthorization{User: r.tgSelfUser(u)}, nil
|
||
}
|
||
|
||
func emailVerificationCode(v tg.EmailVerificationClass) string {
|
||
switch e := v.(type) {
|
||
case *tg.EmailVerificationCode:
|
||
return e.Code
|
||
case *tg.EmailVerificationGoogle:
|
||
return e.Token
|
||
case *tg.EmailVerificationApple:
|
||
return e.Token
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// onAuthImportBotAuthorization 处理 auth.importBotAuthorization:bot 程序凭 token
|
||
// 登录为 bot 账号。api_id/api_hash 与现有 sendCode 行为一致不校验(无 app 注册表)。
|
||
// 收尾与 signIn 同构(Bind 原子切换 update baseline → 授权缓存 → session 绑定),但不写登录消息、不推
|
||
// signIn 服务通知——那是手机登录语义。
|
||
func (r *Router) onAuthImportBotAuthorization(ctx context.Context, req *tg.AuthImportBotAuthorizationRequest) (tg.AuthAuthorizationClass, error) {
|
||
if r.deps.Auth == nil {
|
||
return nil, accessTokenInvalidErr()
|
||
}
|
||
u, err := r.deps.Auth.SignInBot(ctx, r.authzFromCtx(ctx), req.BotAuthToken)
|
||
if err != nil {
|
||
return nil, importBotAuthorizationErr(err)
|
||
}
|
||
if id, ok := AuthKeyIDFrom(ctx); ok {
|
||
r.setAuthUserCache(id, u.ID, true)
|
||
}
|
||
r.bindSessionUser(ctx, u.ID)
|
||
return &tg.AuthAuthorization{User: r.tgSelfUser(u)}, nil
|
||
}
|
||
|
||
// onAuthSignUp 处理 auth.signUp:创建用户并绑定授权。
|
||
func (r *Router) onAuthSignUp(ctx context.Context, req *tg.AuthSignUpRequest) (tg.AuthAuthorizationClass, error) {
|
||
u, loginMessage, err := r.deps.Auth.SignUp(ctx, r.authzFromCtx(ctx), req.PhoneNumber, req.PhoneCodeHash, req.FirstName, req.LastName)
|
||
if err != nil {
|
||
return nil, signInErr(err)
|
||
}
|
||
if id, ok := AuthKeyIDFrom(ctx); ok {
|
||
r.setAuthUserCache(id, u.ID, true)
|
||
}
|
||
r.bindSessionUser(ctx, u.ID)
|
||
r.enqueueLoginMessageBootstrap(ctx, loginMessage)
|
||
return &tg.AuthAuthorization{User: r.tgSelfUser(u)}, nil
|
||
}
|
||
|
||
// onAuthLogOut 处理 auth.logOut:解绑当前 auth_key 的授权。
|
||
func (r *Router) onAuthLogOut(ctx context.Context) (*tg.AuthLoggedOut, error) {
|
||
id, _ := AuthKeyIDFrom(ctx)
|
||
userID, authorized, userErr := r.currentUserID(ctx)
|
||
if err := r.deps.Auth.LogOut(ctx, id); err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
r.invalidateAuthUserCache(id)
|
||
r.unbindAuthKey(id)
|
||
// bot 登出不广播 offline(bot 无 presence 语义,与登录路径对称)。
|
||
if userErr == nil && authorized && userID != 0 && !r.userIsBot(ctx, userID) {
|
||
status, _ := r.setPresenceFromContext(ctx, userID, true, presencePersistSync)
|
||
r.pushUserStatus(ctx, userID, status)
|
||
// 登出后主动清掉本 session 的 presence 条目:连接通常不断开(客户端回登录页),
|
||
// 上面 unbindAuthKey 已把连接 userID 清 0,TCP 真正断开时 SessionOffline 因 userID=0
|
||
// 提前返回、不再清 presence,条目会以 offline 态滞留泄露。这里随登出一并清除。
|
||
if key, ok := presenceSessionKeyFromContext(ctx); ok {
|
||
r.presence.clearSession(key)
|
||
}
|
||
}
|
||
// P1 修复:登出销毁本设备 perm auth_key 后,级联 discard 其绑定的活跃密聊并通知对端
|
||
//(否则对端继续往死 auth_key 投递成静默死链)。best-effort,不阻断登出。
|
||
if userErr == nil && userID != 0 {
|
||
r.discardSecretChatsForAuthKey(ctx, businessAuthKeyInt64(id), userID)
|
||
}
|
||
if err := r.clearAuthKeyState(ctx, id); err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
return &tg.AuthLoggedOut{}, nil
|
||
}
|
||
|
||
func (r *Router) clearAuthKeyState(ctx context.Context, authKeyID [8]byte) error {
|
||
if r.deps.Updates == nil {
|
||
return nil
|
||
}
|
||
return r.deps.Updates.ClearAuthKey(ctx, authKeyID)
|
||
}
|
||
|
||
func (r *Router) bindSessionUser(ctx context.Context, userID int64) {
|
||
if r.deps.Sessions == nil {
|
||
return
|
||
}
|
||
sessionID, ok := SessionIDFrom(ctx)
|
||
if !ok {
|
||
return
|
||
}
|
||
r.deps.Sessions.BindUserForAuthKey(rawAuthKeyIDForOrigin(ctx), sessionID, userID)
|
||
r.announceSessionOnline(ctx, userID)
|
||
}
|
||
|
||
func (r *Router) unbindAuthKey(authKeyID [8]byte) {
|
||
if r.deps.Sessions == nil {
|
||
return
|
||
}
|
||
r.deps.Sessions.UnbindAuthKey(authKeyID)
|
||
}
|
||
|
||
// revokeAuthKeySessions 是授权撤销(被踢设备)的完整失效闭环:清 Router 授权缓存、
|
||
// 清 temp→perm 短缓存、强制断开在线连接、再兜底解绑。断开不可省略——出站推送用
|
||
// 连接持有的密钥加密、不回查授权表,perm-key 连接的授权缓存也只有重连才会重新回查;
|
||
// 不断开的话被踢设备仍能持续收到推送并以缓存身份继续发请求。重连后回查 store 即得
|
||
// 未授权(401)。
|
||
//
|
||
// 顺序关键:先 CloseSessionsForBusinessAuthKey 再 unbindAuthKey。Close 内部 removeLocked
|
||
// 读取连接当前 userID 生成 SessionOffline 事件(驱动 presence 清理与 offline 广播);
|
||
// 若先 unbind 把 userID 清成 0,事件就退化为 userID=0,被踢设备的 presence 条目不被
|
||
// 清理、好友侧最长一个在线 TTL 仍显示其在线。Close 已把连接移出索引,随后的 unbind
|
||
// 对未实现 SessionTerminator 的 Sessions 才有意义(生产实现走 Close 即可,unbind 是 no-op)。
|
||
func (r *Router) revokeAuthKeySessions(authKeyID [8]byte) {
|
||
r.invalidateAuthUserCache(authKeyID)
|
||
rawTempAuthKeyIDs := r.invalidateTempAuthKeyCacheForPerm(authKeyID)
|
||
if terminator, ok := r.deps.Sessions.(SessionTerminator); ok {
|
||
terminator.CloseSessionsForBusinessAuthKey(authKeyID)
|
||
}
|
||
if terminator, ok := r.deps.Sessions.(RawSessionTerminator); ok {
|
||
for _, rawAuthKeyID := range rawTempAuthKeyIDs {
|
||
if rawAuthKeyID == authKeyID {
|
||
continue
|
||
}
|
||
terminator.CloseSessionsForRawAuthKeyExcept(rawAuthKeyID, 0)
|
||
}
|
||
}
|
||
r.unbindAuthKey(authKeyID)
|
||
}
|
||
|
||
func (r *Router) invalidateTempAuthKeyCacheForPerm(authKeyID [8]byte) [][8]byte {
|
||
return r.tempKeyResolveCache.DeleteByPerm(authKeyID)
|
||
}
|
||
|
||
func (r *Router) pushSignInServiceNotificationToOthers(ctx context.Context, u domain.User) {
|
||
if r.deps.Sessions == nil || u.ID == 0 {
|
||
return
|
||
}
|
||
authKeyID, hasAuthKeyID := AuthKeyIDFrom(ctx)
|
||
rawAuthKeyID, hasRawAuthKeyID := RawAuthKeyIDFrom(ctx)
|
||
sessionID, hasSessionID := SessionIDFrom(ctx)
|
||
if !hasAuthKeyID || !hasRawAuthKeyID || !hasSessionID {
|
||
return
|
||
}
|
||
notification := r.tgSignInServiceNotification(ctx, u, authKeyID)
|
||
go func() {
|
||
pushCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||
defer cancel()
|
||
if sent, err := r.deps.Sessions.PushToUserExceptAuthKeySession(pushCtx, u.ID, rawAuthKeyID, sessionID, proto.MessageFromServer, notification); err != nil {
|
||
r.log.Debug("push sign-in service notification", zap.Int64("user_id", u.ID), zap.Int("sent", sent), zap.Error(err))
|
||
}
|
||
}()
|
||
}
|
||
|
||
func (r *Router) tgSignInServiceNotification(ctx context.Context, u domain.User, authKeyID [8]byte) *tg.Updates {
|
||
now := r.clock.Now()
|
||
client := "Unknown device"
|
||
if ci, ok := ClientInfoFrom(ctx); ok {
|
||
parts := []string{}
|
||
if ci.DeviceModel != "" {
|
||
parts = append(parts, branding.UserVisibleText(ci.DeviceModel, ""))
|
||
}
|
||
if ci.SystemVersion != "" {
|
||
parts = append(parts, branding.UserVisibleText(ci.SystemVersion, ""))
|
||
}
|
||
if ci.AppVersion != "" {
|
||
parts = append(parts, branding.UserVisibleText(ci.AppVersion, ""))
|
||
}
|
||
if len(parts) > 0 {
|
||
client = strings.Join(parts, " / ")
|
||
}
|
||
}
|
||
name := strings.TrimSpace(strings.TrimSpace(u.FirstName + " " + u.LastName))
|
||
if name == "" {
|
||
name = u.Phone
|
||
}
|
||
if name == "" {
|
||
name = "there"
|
||
}
|
||
message := fmt.Sprintf("New login.\nDear %s, we detected a login into your account from a new device on %s.\n\nDevice: %s\nLocation: Unknown\n\nIf this wasn't you, you can terminate that session in Settings > Devices (or Privacy & Security > Active Sessions).",
|
||
name,
|
||
now.UTC().Format(time.RFC1123),
|
||
client,
|
||
)
|
||
authID := int64(binary.LittleEndian.Uint64(authKeyID[:]))
|
||
update := &tg.UpdateServiceNotification{
|
||
InboxDate: int(now.Unix()),
|
||
Type: fmt.Sprintf("auth%d_%d", authID, now.Unix()),
|
||
Message: message,
|
||
Media: &tg.MessageMediaEmpty{},
|
||
Entities: signInNotificationEntities(message),
|
||
}
|
||
return &tg.Updates{
|
||
Updates: []tg.UpdateClass{update},
|
||
Date: int(now.Unix()),
|
||
}
|
||
}
|
||
|
||
func signInNotificationEntities(message string) []tg.MessageEntityClass {
|
||
terms := []string{"New login.", "Settings > Devices", "Privacy & Security > Active Sessions"}
|
||
out := make([]tg.MessageEntityClass, 0, len(terms))
|
||
for _, term := range terms {
|
||
if offset := strings.Index(message, term); offset >= 0 {
|
||
out = append(out, &tg.MessageEntityBold{Offset: offset, Length: len(term)})
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
func authKeyIDFromInt64(v int64) [8]byte {
|
||
var id [8]byte
|
||
binary.LittleEndian.PutUint64(id[:], uint64(v))
|
||
return id
|
||
}
|