feat: sync Telegram Login OIDC provider
This commit is contained in:
parent
30774f8c39
commit
ebead9e98c
63 changed files with 11374 additions and 37 deletions
|
|
@ -380,14 +380,10 @@ func (r *Router) registerAccount(d *tlprofile.Dispatcher) {
|
|||
ID)
|
||||
})
|
||||
registerRPC[*tg.AccountGetWebAuthorizationsRequest](d, tlprofile.SemanticMethodAccountGetWebAuthorizations, func(ctx context.Context, layerRequest *tg.AccountGetWebAuthorizationsRequest) (any, error) {
|
||||
return tdesktop.WebAuthorizations(), nil
|
||||
return r.onAccountGetWebAuthorizations(ctx)
|
||||
})
|
||||
registerRPC[*tg.AccountResetWebAuthorizationRequest](d, tlprofile.SemanticMethodAccountResetWebAuthorization, func(ctx context.Context, layerRequest *tg.AccountResetWebAuthorizationRequest) (any, error) {
|
||||
hash := layerRequest.
|
||||
Hash
|
||||
_ = hash
|
||||
|
||||
return true, nil
|
||||
return r.onAccountResetWebAuthorization(ctx, layerRequest.Hash)
|
||||
})
|
||||
registerRPC[*tg.AccountResetWebAuthorizationsRequest](d, tlprofile.SemanticMethodAccountResetWebAuthorizations, func(ctx context.Context, layerRequest *tg.AccountResetWebAuthorizationsRequest) (
|
||||
|
||||
|
|
@ -395,7 +391,7 @@ func (r *Router) registerAccount(d *tlprofile.Dispatcher) {
|
|||
// (无内置浏览器例外、不强制外部浏览器)。Android 启动时会拉取,缺它会反复 500
|
||||
// NOT_IMPLEMENTED。空结构 Hash=0,客户端按默认(内置浏览器、无例外)渲染。
|
||||
any, error) {
|
||||
return true, nil
|
||||
return r.onAccountResetWebAuthorizations(ctx)
|
||||
})
|
||||
registerRPC[*tg.AccountGetWebBrowserSettingsRequest](d, tlprofile.SemanticMethodAccountGetWebBrowserSettings, func(ctx context.Context, layerRequest *tg.AccountGetWebBrowserSettingsRequest) (any, error) {
|
||||
hash := layerRequest.
|
||||
|
|
|
|||
|
|
@ -834,6 +834,14 @@ func (r *Router) BotAPIEditMessageText(ctx context.Context, botID, chatID int64,
|
|||
return domain.Message{}, errors.New("MESSAGE_TOO_LONG")
|
||||
}
|
||||
peer := domain.Peer{Type: domain.PeerTypeUser, ID: chatID}
|
||||
if setReplyMarkup {
|
||||
if err := domain.ValidateReplyMarkup(replyMarkup); err != nil {
|
||||
return domain.Message{}, replyMarkupErr(err)
|
||||
}
|
||||
if err := r.validateReplyMarkupForPeer(ctx, botID, peer, replyMarkup); err != nil {
|
||||
return domain.Message{}, err
|
||||
}
|
||||
}
|
||||
res, err := r.deps.Messages.EditMessage(ctx, botID, domain.EditMessageRequest{
|
||||
OwnerUserID: botID,
|
||||
Peer: peer,
|
||||
|
|
@ -934,6 +942,11 @@ func (r *Router) BotAPIEditInlineMessageText(ctx context.Context, botID int64, i
|
|||
if err := domain.ValidateReplyMarkup(replyMarkup); err != nil {
|
||||
return false, replyMarkupErr(err)
|
||||
}
|
||||
if setReplyMarkup {
|
||||
if err := r.prepareTelegramLoginMarkup(ctx, botID, replyMarkup); err != nil {
|
||||
return false, replyMarkupErr(err)
|
||||
}
|
||||
}
|
||||
req := &tg.MessagesEditInlineBotMessageRequest{
|
||||
ID: tgInputBotInlineMessageID(inlineMessageID),
|
||||
NoWebpage: disableWebPagePreview,
|
||||
|
|
@ -959,6 +972,11 @@ func (r *Router) BotAPIEditInlineRichMessage(ctx context.Context, botID int64, i
|
|||
if err := domain.ValidateReplyMarkup(replyMarkup); err != nil {
|
||||
return false, replyMarkupErr(err)
|
||||
}
|
||||
if setReplyMarkup {
|
||||
if err := r.prepareTelegramLoginMarkup(ctx, botID, replyMarkup); err != nil {
|
||||
return false, replyMarkupErr(err)
|
||||
}
|
||||
}
|
||||
wire, err := tgInputRichMessageFromBotAPI(input)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
|
|
|||
|
|
@ -300,6 +300,9 @@ func (r *Router) domainInlineResultsFromTG(ctx context.Context, botID int64, req
|
|||
if err != nil {
|
||||
return domain.BotInlineResults{}, err
|
||||
}
|
||||
if err := r.prepareTelegramLoginMarkup(ctx, botID, item.ReplyMarkup); err != nil {
|
||||
return domain.BotInlineResults{}, replyMarkupErr(err)
|
||||
}
|
||||
if _, ok := seen[item.ID]; ok {
|
||||
return domain.BotInlineResults{}, resultIDDuplicateErr()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package rpc
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
"github.com/iamxvbaba/td/tgerr"
|
||||
|
|
@ -14,6 +15,9 @@ import (
|
|||
// a chat input field and are not supported in broadcast channels. Inline keyboards remain
|
||||
// valid in both megagroups and broadcasts.
|
||||
func (r *Router) validateReplyMarkupForPeer(ctx context.Context, userID int64, peer domain.Peer, markup *domain.MessageReplyMarkup) error {
|
||||
if err := r.prepareTelegramLoginMarkup(ctx, userID, markup); err != nil {
|
||||
return replyMarkupErr(err)
|
||||
}
|
||||
if markup == nil || !markup.IsReplyKeyboardFamily() || peer.Type != domain.PeerTypeChannel {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -30,6 +34,92 @@ func (r *Router) validateReplyMarkupForPeer(ctx context.Context, userID int64, p
|
|||
return nil
|
||||
}
|
||||
|
||||
// prepareTelegramLoginMarkup resolves every login_url target and validates its
|
||||
// linked web origin before persistence. It mutates only the freshly parsed
|
||||
// request DTO and assigns a deterministic flattened button id, which is later
|
||||
// re-read by messages.requestUrlAuth.
|
||||
func (r *Router) prepareTelegramLoginMarkup(ctx context.Context, senderBotID int64, markup *domain.MessageReplyMarkup) error {
|
||||
if markup == nil || markup.Kind() != domain.MessageReplyMarkupInline {
|
||||
return nil
|
||||
}
|
||||
hasLoginButton := false
|
||||
for rowIndex := range markup.Inline {
|
||||
for buttonIndex := range markup.Inline[rowIndex] {
|
||||
if markup.Inline[rowIndex][buttonIndex].Type == domain.MarkupButtonLoginURL {
|
||||
hasLoginButton = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasLoginButton {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasLoginButton {
|
||||
return nil
|
||||
}
|
||||
if r == nil || r.deps.TelegramLogin == nil || r.deps.Users == nil || senderBotID <= 0 {
|
||||
return domain.ErrButtonTypeInvalid
|
||||
}
|
||||
sender, found, err := r.deps.Users.ByID(ctx, senderBotID, senderBotID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found || !sender.Bot || sender.Deleted {
|
||||
return domain.ErrButtonTypeInvalid
|
||||
}
|
||||
flatID := 0
|
||||
for rowIndex := range markup.Inline {
|
||||
for buttonIndex := range markup.Inline[rowIndex] {
|
||||
button := &markup.Inline[rowIndex][buttonIndex]
|
||||
if button.Type != domain.MarkupButtonLoginURL {
|
||||
flatID++
|
||||
continue
|
||||
}
|
||||
botID := button.LoginBotUserID
|
||||
if button.LoginBotUsername != "" {
|
||||
resolver, ok := r.deps.Users.(UserIdentityService)
|
||||
if !ok {
|
||||
return domain.ErrButtonInvalid
|
||||
}
|
||||
bot, found, err := resolver.ResolveUsername(ctx, senderBotID, strings.TrimPrefix(button.LoginBotUsername, "@"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found || !bot.Bot || bot.Deleted {
|
||||
return domain.ErrButtonInvalid
|
||||
}
|
||||
botID = bot.ID
|
||||
}
|
||||
if botID == 0 {
|
||||
botID = senderBotID
|
||||
}
|
||||
bot, found, err := r.deps.Users.ByID(ctx, senderBotID, botID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found || !bot.Bot || bot.Deleted {
|
||||
return domain.ErrButtonInvalid
|
||||
}
|
||||
normalized, _, err := r.deps.TelegramLogin.ValidateMessageButton(ctx, botID, button.URL)
|
||||
if err != nil {
|
||||
if errors.Is(err, domain.ErrTelegramLoginURLInvalid) || errors.Is(err, domain.ErrTelegramLoginOriginNotAllowed) {
|
||||
return domain.ErrButtonURLInvalid
|
||||
}
|
||||
if errors.Is(err, domain.ErrTelegramLoginClientDisabled) {
|
||||
return domain.ErrButtonInvalid
|
||||
}
|
||||
return err
|
||||
}
|
||||
button.URL = normalized
|
||||
button.LoginBotUserID = botID
|
||||
button.LoginBotUsername = ""
|
||||
button.ButtonID = flatID
|
||||
flatID++
|
||||
}
|
||||
}
|
||||
return domain.ValidateReplyMarkup(markup)
|
||||
}
|
||||
|
||||
// P3 reply_markup 错误码(对齐官方)。
|
||||
func buttonDataInvalidErr() error { return tgerr.New(400, "BUTTON_DATA_INVALID") }
|
||||
func buttonInvalidErr() error { return tgerr.New(400, "BUTTON_INVALID") }
|
||||
|
|
@ -175,21 +265,23 @@ func domainReplyKeyboardButton(button tg.KeyboardButtonClass) (domain.MarkupButt
|
|||
|
||||
func domainInlineMarkup(inline *tg.ReplyInlineMarkup) (*domain.MessageReplyMarkup, error) {
|
||||
out := &domain.MessageReplyMarkup{Type: domain.MessageReplyMarkupInline, Inline: make([][]domain.MarkupButton, 0, len(inline.Rows))}
|
||||
buttonID := 0
|
||||
for _, row := range inline.Rows {
|
||||
domainRow := make([]domain.MarkupButton, 0, len(row.Buttons))
|
||||
for _, btn := range row.Buttons {
|
||||
db, err := domainMarkupButton(btn)
|
||||
db, err := domainMarkupButton(btn, buttonID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
domainRow = append(domainRow, db)
|
||||
buttonID++
|
||||
}
|
||||
out.Inline = append(out.Inline, domainRow)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func domainMarkupButton(btn tg.KeyboardButtonClass) (domain.MarkupButton, error) {
|
||||
func domainMarkupButton(btn tg.KeyboardButtonClass, buttonID int) (domain.MarkupButton, error) {
|
||||
style, icon, err := domainMarkupButtonStyle(btn)
|
||||
if err != nil {
|
||||
return domain.MarkupButton{}, err
|
||||
|
|
@ -209,6 +301,26 @@ func domainMarkupButton(btn tg.KeyboardButtonClass) (domain.MarkupButton, error)
|
|||
Type: domain.MarkupButtonURL, Text: b.Text, URL: b.URL,
|
||||
Style: style, IconCustomEmojiID: icon,
|
||||
}, nil
|
||||
case *tg.InputKeyboardButtonURLAuth:
|
||||
botUserID := int64(0)
|
||||
switch bot := b.Bot.(type) {
|
||||
case nil, *tg.InputUserEmpty, *tg.InputUserSelf:
|
||||
case *tg.InputUser:
|
||||
botUserID = bot.UserID
|
||||
default:
|
||||
return domain.MarkupButton{}, domain.ErrButtonInvalid
|
||||
}
|
||||
return domain.MarkupButton{
|
||||
Type: domain.MarkupButtonLoginURL, Text: b.Text, URL: b.URL,
|
||||
ForwardText: b.FwdText, ButtonID: buttonID, LoginBotUserID: botUserID,
|
||||
RequestWriteAccess: b.RequestWriteAccess, Style: style, IconCustomEmojiID: icon,
|
||||
}, nil
|
||||
case *tg.KeyboardButtonURLAuth:
|
||||
return domain.MarkupButton{
|
||||
Type: domain.MarkupButtonLoginURL, Text: b.Text, URL: b.URL,
|
||||
ForwardText: b.FwdText, ButtonID: b.ButtonID,
|
||||
Style: style, IconCustomEmojiID: icon,
|
||||
}, nil
|
||||
case *tg.KeyboardButtonWebView:
|
||||
return domain.MarkupButton{Type: domain.MarkupButtonWebView, Text: b.Text, URL: b.URL, Style: style, IconCustomEmojiID: icon}, nil
|
||||
case *tg.KeyboardButtonSwitchInline:
|
||||
|
|
@ -322,6 +434,15 @@ func tgMarkupButton(btn domain.MarkupButton) tg.KeyboardButtonClass {
|
|||
out.SetStyle(style)
|
||||
}
|
||||
return out
|
||||
case domain.MarkupButtonLoginURL:
|
||||
out := &tg.KeyboardButtonURLAuth{Text: btn.Text, URL: btn.URL, ButtonID: btn.ButtonID}
|
||||
if btn.ForwardText != "" {
|
||||
out.SetFwdText(btn.ForwardText)
|
||||
}
|
||||
if style, ok := tgMarkupButtonStyle(btn); ok {
|
||||
out.SetStyle(style)
|
||||
}
|
||||
return out
|
||||
case domain.MarkupButtonWebView:
|
||||
out := &tg.KeyboardButtonWebView{Text: btn.Text, URL: btn.URL}
|
||||
if style, ok := tgMarkupButtonStyle(btn); ok {
|
||||
|
|
|
|||
|
|
@ -71,6 +71,26 @@ func TestInlineButtonStyleTLDomainRoundTrip(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLoginURLButtonTLDomainProjection(t *testing.T) {
|
||||
button := &tg.InputKeyboardButtonURLAuth{
|
||||
Text: "Log in", URL: "https://example.com/login", Bot: &tg.InputUser{UserID: 9001, AccessHash: 77},
|
||||
}
|
||||
button.SetRequestWriteAccess(true)
|
||||
button.SetFwdText("Open login")
|
||||
markup, err := domainReplyMarkupForSender(&tg.ReplyInlineMarkup{Rows: []tg.KeyboardButtonRow{{Buttons: []tg.KeyboardButtonClass{button}}}}, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := markup.Inline[0][0]
|
||||
if got.Type != domain.MarkupButtonLoginURL || got.LoginBotUserID != 9001 || !got.RequestWriteAccess || got.ForwardText != "Open login" || got.ButtonID != 0 {
|
||||
t.Fatalf("domain login_url = %#v", got)
|
||||
}
|
||||
wire, ok := tgReplyMarkup(markup).(*tg.ReplyInlineMarkup).Rows[0].Buttons[0].(*tg.KeyboardButtonURLAuth)
|
||||
if !ok || wire.Text != "Log in" || wire.URL != "https://example.com/login" || wire.ButtonID != 0 || wire.FwdText != "Open login" {
|
||||
t.Fatalf("wire login_url = %#v", wire)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplyKeyboardHideAndForceReplyTLDomainRoundTrip(t *testing.T) {
|
||||
hide, err := domainOutgoingReplyMarkupForSender(&tg.ReplyKeyboardHide{Selective: true}, true)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -241,6 +241,24 @@ type UsersService interface {
|
|||
ByIDs(ctx context.Context, currentUserID int64, userIDs []int64) ([]domain.User, error)
|
||||
}
|
||||
|
||||
// TelegramLoginService is the domain-only boundary shared by the MTProto RPC
|
||||
// edge and the public OIDC provider. PostgreSQL remains authoritative for all
|
||||
// consent transitions; the RPC layer only projects domain state to TL.
|
||||
type TelegramLoginService interface {
|
||||
ValidateMessageButton(ctx context.Context, botUserID int64, rawURL string) (normalizedURL, domainName string, err error)
|
||||
AuthorizeMessageButton(ctx context.Context, params domain.TelegramLoginMessageButtonAuthorization) (domain.TelegramLoginMessageButtonResult, error)
|
||||
RequestByDeepLink(ctx context.Context, deepLink string) (domain.TelegramLoginRequest, error)
|
||||
RequestByDeepLinkForOrigin(ctx context.Context, deepLink, inAppOrigin string) (domain.TelegramLoginRequest, error)
|
||||
CheckMatchCode(ctx context.Context, deepLink, selected string) (bool, error)
|
||||
Approve(ctx context.Context, deepLink string, identity domain.TelegramLoginIdentitySnapshot, writeAllowed, phoneShared bool, matchCode string) (domain.TelegramLoginRequest, domain.TelegramLoginWebAuthorization, error)
|
||||
FinalizeRedirectByDeepLink(ctx context.Context, deepLink string) (string, error)
|
||||
FinalizeInAppRedirectByDeepLink(ctx context.Context, deepLink string) (string, error)
|
||||
Decline(ctx context.Context, deepLink string, userID int64) (domain.TelegramLoginRequest, error)
|
||||
ListWebAuthorizations(ctx context.Context, userID int64) ([]domain.TelegramLoginWebAuthorization, error)
|
||||
RevokeWebAuthorization(ctx context.Context, userID, hash int64) error
|
||||
RevokeAllWebAuthorizations(ctx context.Context, userID int64) (int64, error)
|
||||
}
|
||||
|
||||
// BatchViewerUsersResolver 是 UsersService 的可选能力:跨多个 viewer 一次性投影同一组 user
|
||||
// (fan-out 模板化,把 per-recipient 的 ByIDs(=ForViewer) 折叠成 O(owner) 查询)。结果按 viewer
|
||||
// 与 ByIDs(viewer, ids) 字节等价(personal photo overlay 除外,见 users.ByIDsForViewers)。
|
||||
|
|
@ -873,6 +891,7 @@ type Deps struct {
|
|||
EphemeralPush store.EphemeralPushBroker
|
||||
EphemeralReports store.EphemeralReportStore
|
||||
Users UsersService
|
||||
TelegramLogin TelegramLoginService
|
||||
Updates UpdatesService
|
||||
BootstrapUpdates store.BootstrapUpdateJobStore
|
||||
BotAPIUpdates store.BotAPIUpdateStore
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ func (r *Router) onMessagesSavePreparedInlineMessage(ctx context.Context, req *t
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := r.prepareTelegramLoginMarkup(ctx, botID, result.ReplyMarkup); err != nil {
|
||||
return nil, replyMarkupErr(err)
|
||||
}
|
||||
peerTypes, err := preparedInlinePeerTypesFromTG(req.PeerTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -159,6 +162,11 @@ func (r *Router) editPrivateInlineBotMessage(ctx context.Context, botID int64, t
|
|||
setReplyMarkup = true
|
||||
}
|
||||
}
|
||||
if setReplyMarkup {
|
||||
if err := r.prepareTelegramLoginMarkup(ctx, botID, replyMarkup); err != nil {
|
||||
return false, replyMarkupErr(err)
|
||||
}
|
||||
}
|
||||
_, err = r.deps.Messages.EditMessage(ctx, target.OwnerUserID, domain.EditMessageRequest{
|
||||
OwnerUserID: target.OwnerUserID,
|
||||
Peer: target.Peer,
|
||||
|
|
@ -246,6 +254,11 @@ func (r *Router) editChannelInlineBotMessage(ctx context.Context, botID int64, t
|
|||
setReplyMarkup = true
|
||||
}
|
||||
}
|
||||
if setReplyMarkup {
|
||||
if err := r.prepareTelegramLoginMarkup(ctx, botID, replyMarkup); err != nil {
|
||||
return false, replyMarkupErr(err)
|
||||
}
|
||||
}
|
||||
res, err := r.deps.Channels.EditInlineBotMessage(ctx, botID, domain.EditChannelMessageRequest{
|
||||
UserID: target.SenderUserID,
|
||||
ChannelID: target.ChannelID,
|
||||
|
|
|
|||
|
|
@ -110,6 +110,11 @@ func (r *Router) onMessagesEditMessage(ctx context.Context, req *tg.MessagesEdit
|
|||
setReplyMarkup = true
|
||||
}
|
||||
}
|
||||
if setReplyMarkup {
|
||||
if err := r.validateReplyMarkupForPeer(ctx, userID, peer, replyMarkup); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if peer.Type == domain.PeerTypeChannel {
|
||||
if r.deps.Channels == nil {
|
||||
return nil, peerIDInvalidErr()
|
||||
|
|
@ -125,6 +130,8 @@ func (r *Router) onMessagesEditMessage(ctx context.Context, req *tg.MessagesEdit
|
|||
Message: message,
|
||||
Entities: domainMessageEntitiesForViewer(userID, entities),
|
||||
MentionUserIDs: mentionUserIDs,
|
||||
SetReplyMarkup: setReplyMarkup,
|
||||
ReplyMarkup: replyMarkup,
|
||||
SetRichMessage: replaceRichMessage,
|
||||
RichMessage: richMessage,
|
||||
EditDate: int(r.clock.Now().Unix()),
|
||||
|
|
|
|||
|
|
@ -11,6 +11,18 @@ import (
|
|||
|
||||
// registerMessages 注册 messages.* RPC handler。
|
||||
func (r *Router) registerMessages(d *tlprofile.Dispatcher) {
|
||||
registerRPC[*tg.MessagesRequestURLAuthRequest](d, tlprofile.SemanticMethodMessagesRequestURLAuth, func(ctx context.Context, req *tg.MessagesRequestURLAuthRequest) (any, error) {
|
||||
return r.onMessagesRequestURLAuth(ctx, req)
|
||||
})
|
||||
registerRPC[*tg.MessagesAcceptURLAuthRequest](d, tlprofile.SemanticMethodMessagesAcceptURLAuth, func(ctx context.Context, req *tg.MessagesAcceptURLAuthRequest) (any, error) {
|
||||
return r.onMessagesAcceptURLAuth(ctx, req)
|
||||
})
|
||||
registerRPC[*tg.MessagesDeclineURLAuthRequest](d, tlprofile.SemanticMethodMessagesDeclineURLAuth, func(ctx context.Context, req *tg.MessagesDeclineURLAuthRequest) (any, error) {
|
||||
return r.onMessagesDeclineURLAuth(ctx, req.URL)
|
||||
})
|
||||
registerRPC[*tg.MessagesCheckURLAuthMatchCodeRequest](d, tlprofile.SemanticMethodMessagesCheckURLAuthMatchCode, func(ctx context.Context, req *tg.MessagesCheckURLAuthMatchCodeRequest) (any, error) {
|
||||
return r.onMessagesCheckURLAuthMatchCode(ctx, req.URL, req.MatchCode)
|
||||
})
|
||||
registerRPC[*tg.MessagesReceivedMessagesRequest](d, tlprofile.SemanticMethodMessagesReceivedMessages, func(ctx context.Context, layerRequest *tg.MessagesReceivedMessagesRequest) (any, error) {
|
||||
return r.onMessagesReceivedMessages(ctx, layerRequest.
|
||||
MaxID)
|
||||
|
|
|
|||
|
|
@ -314,6 +314,9 @@ func (r *Router) onMessagesSendWebViewResultMessage(ctx context.Context, req *tg
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := r.prepareTelegramLoginMarkup(ctx, botID, result.ReplyMarkup); err != nil {
|
||||
return nil, replyMarkupErr(err)
|
||||
}
|
||||
if err := r.sendWebViewDomainResultMessage(ctx, botID, req.BotQueryID, result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -336,6 +339,9 @@ func (r *Router) AnswerWebAppQueryFromBotAPI(ctx context.Context, botID int64, b
|
|||
} else if !found {
|
||||
return "", userBotRequiredErr()
|
||||
}
|
||||
if err := r.prepareTelegramLoginMarkup(ctx, botID, result.ReplyMarkup); err != nil {
|
||||
return "", replyMarkupErr(err)
|
||||
}
|
||||
if err := r.sendWebViewDomainResultMessage(ctx, botID, botQueryID, result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -362,6 +368,9 @@ func (r *Router) SavePreparedInlineMessageFromBotAPI(ctx context.Context, botID,
|
|||
} else if !found {
|
||||
return "", 0, userIDInvalidErr()
|
||||
}
|
||||
if err := r.prepareTelegramLoginMarkup(ctx, botID, result.ReplyMarkup); err != nil {
|
||||
return "", 0, replyMarkupErr(err)
|
||||
}
|
||||
id, expireDate := r.inlines.savePreparedInlineContext(ctx, r.clock.Now(), botID, userID, result, peerTypes)
|
||||
return id, expireDate, nil
|
||||
}
|
||||
|
|
|
|||
434
internal/rpc/telegram_login.go
Normal file
434
internal/rpc/telegram_login.go
Normal file
|
|
@ -0,0 +1,434 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
"github.com/iamxvbaba/td/tgerr"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
func telegramLoginOAuthInvalidErr() error { return tgerr.New(500, "OAUTH_REQUEST_INVALID") }
|
||||
func telegramLoginURLExpiredErr() error { return tgerr.New(400, "URL_EXPIRED") }
|
||||
func telegramLoginURLInvalidErr() error { return tgerr.New(400, "URL_INVALID") }
|
||||
func telegramLoginHashInvalidErr() error { return tgerr.New(400, "HASH_INVALID") }
|
||||
|
||||
func telegramLoginRPCError(err error) error {
|
||||
switch {
|
||||
case errors.Is(err, domain.ErrTelegramLoginRequestExpired):
|
||||
return telegramLoginURLExpiredErr()
|
||||
case errors.Is(err, domain.ErrTelegramLoginURLInvalid):
|
||||
return telegramLoginURLInvalidErr()
|
||||
case errors.Is(err, domain.ErrTelegramLoginMatchCodeInvalid),
|
||||
errors.Is(err, domain.ErrTelegramLoginRequestInvalid),
|
||||
errors.Is(err, domain.ErrTelegramLoginRequestConflict),
|
||||
errors.Is(err, domain.ErrTelegramLoginClientDisabled),
|
||||
errors.Is(err, domain.ErrTelegramLoginOriginNotAllowed),
|
||||
errors.Is(err, domain.ErrTelegramLoginRedirectNotAllowed),
|
||||
errors.Is(err, domain.ErrTelegramLoginScopeInvalid),
|
||||
errors.Is(err, domain.ErrTelegramLoginAuthorizationsTooMany):
|
||||
return telegramLoginOAuthInvalidErr()
|
||||
default:
|
||||
return internalErr()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) requireTelegramLoginUser(ctx context.Context) (int64, error) {
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil || userID <= 0 || r.deps.TelegramLogin == nil || r.deps.Users == nil {
|
||||
return 0, internalErr()
|
||||
}
|
||||
self, err := r.deps.Users.Self(ctx, userID)
|
||||
if err != nil || self.Bot || self.Deleted {
|
||||
return 0, telegramLoginOAuthInvalidErr()
|
||||
}
|
||||
return userID, nil
|
||||
}
|
||||
|
||||
func (r *Router) telegramLoginRequestResult(ctx context.Context, viewerUserID int64, request domain.TelegramLoginRequest, deepLink string) (tg.URLAuthResultClass, error) {
|
||||
switch request.Status {
|
||||
case domain.TelegramLoginRequestApproved:
|
||||
if request.AuthorizedUserID != viewerUserID {
|
||||
return nil, telegramLoginOAuthInvalidErr()
|
||||
}
|
||||
return r.telegramLoginAcceptedResult(ctx, request, deepLink)
|
||||
case domain.TelegramLoginRequestPending:
|
||||
// Continue below.
|
||||
case domain.TelegramLoginRequestDeclined, domain.TelegramLoginRequestExpired:
|
||||
return nil, telegramLoginURLExpiredErr()
|
||||
default:
|
||||
return nil, telegramLoginOAuthInvalidErr()
|
||||
}
|
||||
bot, found, err := r.deps.Users.ByID(ctx, viewerUserID, request.BotUserID)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if !found || !bot.Bot || bot.Deleted {
|
||||
return nil, telegramLoginOAuthInvalidErr()
|
||||
}
|
||||
botTL := r.withBotProfileFlags(ctx, r.tgUser(bot))
|
||||
out := &tg.URLAuthResultRequest{
|
||||
RequestWriteAccess: request.Requests(domain.TelegramLoginScopeBotAccess),
|
||||
RequestPhoneNumber: request.Requests(domain.TelegramLoginScopePhone),
|
||||
MatchCodesFirst: request.MatchCodesFirst,
|
||||
IsApp: request.IsApp,
|
||||
Bot: botTL,
|
||||
Domain: request.Domain,
|
||||
}
|
||||
// OAuth requests carry the complete device tuple. Keep the four fields on
|
||||
// their shared flag together so old exact-layer codecs never see a partial
|
||||
// conditional shape.
|
||||
if request.Browser != "" && request.Platform != "" && request.IP != "" && request.Region != "" {
|
||||
out.SetBrowser(request.Browser)
|
||||
out.SetPlatform(request.Platform)
|
||||
out.SetIP(request.IP)
|
||||
out.SetRegion(request.Region)
|
||||
}
|
||||
if len(request.MatchCodes) > 0 {
|
||||
out.SetMatchCodes(append([]string(nil), request.MatchCodes...))
|
||||
}
|
||||
if request.UserIDHint > 0 {
|
||||
out.SetUserIDHint(request.UserIDHint)
|
||||
}
|
||||
if request.IsApp && request.VerifiedAppName != "" {
|
||||
out.SetVerifiedAppName(request.VerifiedAppName)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *Router) telegramLoginAcceptedResult(ctx context.Context, request domain.TelegramLoginRequest, deepLink string) (tg.URLAuthResultClass, error) {
|
||||
accepted := &tg.URLAuthResultAccepted{}
|
||||
switch {
|
||||
case request.Source == domain.TelegramLoginRequestNative && request.IsApp:
|
||||
redirectURL, err := r.deps.TelegramLogin.FinalizeRedirectByDeepLink(ctx, deepLink)
|
||||
if err != nil {
|
||||
return nil, telegramLoginRPCError(err)
|
||||
}
|
||||
accepted.SetURL(redirectURL)
|
||||
case request.Source == domain.TelegramLoginRequestMiniApp:
|
||||
resultURL, err := r.deps.TelegramLogin.FinalizeInAppRedirectByDeepLink(ctx, deepLink)
|
||||
if err != nil {
|
||||
return nil, telegramLoginRPCError(err)
|
||||
}
|
||||
accepted.SetURL(resultURL)
|
||||
}
|
||||
return accepted, nil
|
||||
}
|
||||
|
||||
func (r *Router) onMessagesRequestURLAuth(ctx context.Context, req *tg.MessagesRequestURLAuthRequest) (tg.URLAuthResultClass, error) {
|
||||
userID, err := r.requireTelegramLoginUser(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, hasPeer := req.GetPeer()
|
||||
urlValue, hasURL := req.GetURL()
|
||||
_, hasOrigin := req.GetInAppOrigin()
|
||||
if hasPeer == hasURL || (!hasPeer && strings.TrimSpace(urlValue) == "") || hasOrigin && !hasURL {
|
||||
return nil, telegramLoginOAuthInvalidErr()
|
||||
}
|
||||
if hasPeer {
|
||||
button, peer, err := r.telegramLoginButtonFromMessage(ctx, userID, req.Peer, req.MsgID, req.ButtonID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u, err := url.Parse(button.URL)
|
||||
if err != nil || u.Hostname() == "" {
|
||||
return nil, telegramLoginURLInvalidErr()
|
||||
}
|
||||
request := domain.TelegramLoginRequest{
|
||||
BotUserID: button.LoginBotUserID, Source: domain.TelegramLoginRequestMessageButton,
|
||||
ResponseType: "legacy_url", RedirectURI: button.URL, Domain: u.Hostname(),
|
||||
Scopes: []domain.TelegramLoginScope{domain.TelegramLoginScopeOpenID, domain.TelegramLoginScopeProfile},
|
||||
PeerType: peer.Type, PeerID: peer.ID, MessageID: req.MsgID, ButtonID: req.ButtonID,
|
||||
Status: domain.TelegramLoginRequestPending,
|
||||
}
|
||||
if button.RequestWriteAccess {
|
||||
request.Scopes = append(request.Scopes, domain.TelegramLoginScopeBotAccess)
|
||||
}
|
||||
return r.telegramLoginRequestResult(ctx, userID, request, "")
|
||||
}
|
||||
if hasOrigin && req.InAppOrigin == "" {
|
||||
return nil, telegramLoginURLInvalidErr()
|
||||
}
|
||||
request, err := r.deps.TelegramLogin.RequestByDeepLinkForOrigin(ctx, urlValue, req.InAppOrigin)
|
||||
if err != nil {
|
||||
return nil, telegramLoginRPCError(err)
|
||||
}
|
||||
return r.telegramLoginRequestResult(ctx, userID, request, urlValue)
|
||||
}
|
||||
|
||||
func (r *Router) onMessagesAcceptURLAuth(ctx context.Context, req *tg.MessagesAcceptURLAuthRequest) (tg.URLAuthResultClass, error) {
|
||||
userID, err := r.requireTelegramLoginUser(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, hasPeer := req.GetPeer()
|
||||
deepLink, hasURL := req.GetURL()
|
||||
matchCode, hasMatchCode := req.GetMatchCode()
|
||||
if hasPeer == hasURL || (!hasPeer && strings.TrimSpace(deepLink) == "") || (hasMatchCode && matchCode == "") {
|
||||
return nil, telegramLoginOAuthInvalidErr()
|
||||
}
|
||||
if hasPeer {
|
||||
if hasMatchCode || req.SharePhoneNumber {
|
||||
return nil, telegramLoginOAuthInvalidErr()
|
||||
}
|
||||
button, peer, err := r.telegramLoginButtonFromMessage(ctx, userID, req.Peer, req.MsgID, req.ButtonID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.deps.Bots == nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
profile, found, err := r.deps.Bots.BotInfo(ctx, button.LoginBotUserID)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if !found || profile.TokenSecret == "" {
|
||||
return nil, telegramLoginOAuthInvalidErr()
|
||||
}
|
||||
self, err := r.deps.Users.Self(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
identity := r.telegramLoginIdentity(self)
|
||||
result, err := r.deps.TelegramLogin.AuthorizeMessageButton(ctx, domain.TelegramLoginMessageButtonAuthorization{
|
||||
UserID: userID, BotUserID: button.LoginBotUserID,
|
||||
BotToken: domain.FormatBotToken(button.LoginBotUserID, profile.TokenSecret), URL: button.URL,
|
||||
RequestWriteAccess: button.RequestWriteAccess, WriteAllowed: req.WriteAllowed,
|
||||
Peer: peer, MessageID: req.MsgID, ButtonID: req.ButtonID,
|
||||
Browser: "Telegram", Platform: "Telegram Client", IP: "Unknown IP", Region: "Unknown region",
|
||||
Identity: identity,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, telegramLoginRPCError(err)
|
||||
}
|
||||
accepted := &tg.URLAuthResultAccepted{}
|
||||
accepted.SetURL(result.URL)
|
||||
return accepted, nil
|
||||
}
|
||||
request, err := r.deps.TelegramLogin.RequestByDeepLink(ctx, deepLink)
|
||||
if err != nil {
|
||||
return nil, telegramLoginRPCError(err)
|
||||
}
|
||||
if request.Status == domain.TelegramLoginRequestApproved {
|
||||
if request.AuthorizedUserID == userID {
|
||||
return r.telegramLoginAcceptedResult(ctx, request, deepLink)
|
||||
}
|
||||
return nil, telegramLoginOAuthInvalidErr()
|
||||
}
|
||||
if request.Status != domain.TelegramLoginRequestPending {
|
||||
return nil, telegramLoginURLExpiredErr()
|
||||
}
|
||||
self, err := r.deps.Users.Self(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
identity := r.telegramLoginIdentity(self)
|
||||
approved, _, err := r.deps.TelegramLogin.Approve(ctx, deepLink, identity, req.WriteAllowed, req.SharePhoneNumber, matchCode)
|
||||
if err != nil {
|
||||
return nil, telegramLoginRPCError(err)
|
||||
}
|
||||
if approved.AuthorizedUserID != userID {
|
||||
return nil, telegramLoginOAuthInvalidErr()
|
||||
}
|
||||
return r.telegramLoginAcceptedResult(ctx, approved, deepLink)
|
||||
}
|
||||
|
||||
func (r *Router) telegramLoginIdentity(self domain.User) domain.TelegramLoginIdentitySnapshot {
|
||||
identity := domain.TelegramLoginIdentitySnapshot{
|
||||
UserID: self.ID, Name: strings.TrimSpace(strings.TrimSpace(self.FirstName) + " " + strings.TrimSpace(self.LastName)),
|
||||
GivenName: self.FirstName, FamilyName: self.LastName,
|
||||
PreferredUsername: self.Username, PhoneNumber: self.Phone,
|
||||
}
|
||||
if strings.TrimSpace(r.cfg.PublicBaseURL) != "" && self.Username != "" && self.PhotoID > 0 {
|
||||
identity.Picture = strings.TrimSuffix(r.cfg.PublicBaseURL, "/") + "/_public/avatar/" + url.PathEscape(self.Username) + "/" + strconv.FormatInt(self.PhotoID, 10)
|
||||
}
|
||||
return identity
|
||||
}
|
||||
|
||||
func (r *Router) telegramLoginButtonFromMessage(ctx context.Context, userID int64, inputPeer tg.InputPeerClass, messageID, buttonID int) (domain.MarkupButton, domain.Peer, error) {
|
||||
if messageID <= 0 || messageID > domain.MaxMessageBoxID || buttonID < 0 {
|
||||
return domain.MarkupButton{}, domain.Peer{}, telegramLoginOAuthInvalidErr()
|
||||
}
|
||||
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, inputPeer)
|
||||
if err != nil {
|
||||
return domain.MarkupButton{}, domain.Peer{}, err
|
||||
}
|
||||
var markup *domain.MessageReplyMarkup
|
||||
switch peer.Type {
|
||||
case domain.PeerTypeUser:
|
||||
message, found, err := r.lookupOwnerMessage(ctx, userID, messageID)
|
||||
if err != nil {
|
||||
return domain.MarkupButton{}, domain.Peer{}, internalErr()
|
||||
}
|
||||
if !found || message.Peer != peer {
|
||||
return domain.MarkupButton{}, domain.Peer{}, telegramLoginOAuthInvalidErr()
|
||||
}
|
||||
markup = message.ReplyMarkup
|
||||
case domain.PeerTypeChannel:
|
||||
if r.deps.Channels == nil {
|
||||
return domain.MarkupButton{}, domain.Peer{}, telegramLoginOAuthInvalidErr()
|
||||
}
|
||||
history, err := r.deps.Channels.GetMessages(ctx, userID, peer.ID, []int{messageID})
|
||||
if err != nil || len(history.Messages) != 1 || history.Messages[0].ID != messageID {
|
||||
return domain.MarkupButton{}, domain.Peer{}, telegramLoginOAuthInvalidErr()
|
||||
}
|
||||
markup = history.Messages[0].ReplyMarkup
|
||||
default:
|
||||
return domain.MarkupButton{}, domain.Peer{}, telegramLoginOAuthInvalidErr()
|
||||
}
|
||||
if markup == nil || markup.Kind() != domain.MessageReplyMarkupInline {
|
||||
return domain.MarkupButton{}, domain.Peer{}, telegramLoginOAuthInvalidErr()
|
||||
}
|
||||
for _, row := range markup.Inline {
|
||||
for _, button := range row {
|
||||
if button.Type == domain.MarkupButtonLoginURL && button.ButtonID == buttonID && button.LoginBotUserID > 0 {
|
||||
return button, peer, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return domain.MarkupButton{}, domain.Peer{}, telegramLoginOAuthInvalidErr()
|
||||
}
|
||||
|
||||
func (r *Router) onMessagesDeclineURLAuth(ctx context.Context, deepLink string) (bool, error) {
|
||||
userID, err := r.requireTelegramLoginUser(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if strings.TrimSpace(deepLink) == "" {
|
||||
return false, telegramLoginURLInvalidErr()
|
||||
}
|
||||
request, err := r.deps.TelegramLogin.RequestByDeepLink(ctx, deepLink)
|
||||
if err != nil {
|
||||
return false, telegramLoginRPCError(err)
|
||||
}
|
||||
if request.Status == domain.TelegramLoginRequestDeclined {
|
||||
return true, nil
|
||||
}
|
||||
if request.Status != domain.TelegramLoginRequestPending {
|
||||
return false, telegramLoginOAuthInvalidErr()
|
||||
}
|
||||
if _, err := r.deps.TelegramLogin.Decline(ctx, deepLink, userID); err != nil {
|
||||
return false, telegramLoginRPCError(err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *Router) onMessagesCheckURLAuthMatchCode(ctx context.Context, deepLink, matchCode string) (bool, error) {
|
||||
if _, err := r.requireTelegramLoginUser(ctx); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if strings.TrimSpace(deepLink) == "" || matchCode == "" {
|
||||
return false, telegramLoginURLInvalidErr()
|
||||
}
|
||||
ok, err := r.deps.TelegramLogin.CheckMatchCode(ctx, deepLink, matchCode)
|
||||
if err != nil {
|
||||
return false, telegramLoginRPCError(err)
|
||||
}
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
func (r *Router) onAccountGetWebAuthorizations(ctx context.Context) (*tg.AccountWebAuthorizations, error) {
|
||||
if r.deps.TelegramLogin == nil {
|
||||
if _, _, err := r.currentUserID(ctx); err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
return &tg.AccountWebAuthorizations{Authorizations: []tg.WebAuthorization{}, Users: []tg.UserClass{}}, nil
|
||||
}
|
||||
userID, err := r.requireTelegramLoginUser(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authorizations, err := r.deps.TelegramLogin.ListWebAuthorizations(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
result := &tg.AccountWebAuthorizations{
|
||||
Authorizations: make([]tg.WebAuthorization, 0, len(authorizations)),
|
||||
Users: []tg.UserClass{},
|
||||
}
|
||||
botIDs := make([]int64, 0, len(authorizations))
|
||||
seenBots := make(map[int64]struct{}, len(authorizations))
|
||||
for _, authorization := range authorizations {
|
||||
result.Authorizations = append(result.Authorizations, tg.WebAuthorization{
|
||||
Hash: authorization.Hash, BotID: authorization.BotUserID, Domain: authorization.Domain,
|
||||
Browser: authorization.Browser, Platform: authorization.Platform,
|
||||
DateCreated: telegramLoginUnixInt(authorization.CreatedAt), DateActive: telegramLoginUnixInt(authorization.LastActiveAt),
|
||||
IP: authorization.IP, Region: authorization.Region,
|
||||
})
|
||||
if _, duplicate := seenBots[authorization.BotUserID]; !duplicate {
|
||||
seenBots[authorization.BotUserID] = struct{}{}
|
||||
botIDs = append(botIDs, authorization.BotUserID)
|
||||
}
|
||||
}
|
||||
if len(botIDs) > 0 {
|
||||
bots, err := r.deps.Users.ByIDs(ctx, userID, botIDs)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
for _, bot := range bots {
|
||||
if bot.Bot && !bot.Deleted {
|
||||
result.Users = append(result.Users, r.withBotProfileFlags(ctx, r.tgUser(bot)))
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *Router) onAccountResetWebAuthorization(ctx context.Context, hash int64) (bool, error) {
|
||||
if r.deps.TelegramLogin == nil {
|
||||
if _, _, err := r.currentUserID(ctx); err != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
userID, err := r.requireTelegramLoginUser(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if hash == 0 {
|
||||
return false, telegramLoginHashInvalidErr()
|
||||
}
|
||||
if err := r.deps.TelegramLogin.RevokeWebAuthorization(ctx, userID, hash); err != nil {
|
||||
if errors.Is(err, domain.ErrTelegramLoginWebAuthHashInvalid) {
|
||||
return false, telegramLoginHashInvalidErr()
|
||||
}
|
||||
return false, internalErr()
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *Router) onAccountResetWebAuthorizations(ctx context.Context) (bool, error) {
|
||||
if r.deps.TelegramLogin == nil {
|
||||
if _, _, err := r.currentUserID(ctx); err != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
userID, err := r.requireTelegramLoginUser(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if _, err := r.deps.TelegramLogin.RevokeAllWebAuthorizations(ctx, userID); err != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func telegramLoginUnixInt(value time.Time) int {
|
||||
unix := value.Unix()
|
||||
if unix < 0 {
|
||||
return 0
|
||||
}
|
||||
if unix > math.MaxInt32 {
|
||||
return math.MaxInt32
|
||||
}
|
||||
return int(unix)
|
||||
}
|
||||
380
internal/rpc/telegram_login_rpc_test.go
Normal file
380
internal/rpc/telegram_login_rpc_test.go
Normal file
|
|
@ -0,0 +1,380 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/iamxvbaba/td/clock"
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
"github.com/iamxvbaba/td/tlprofile"
|
||||
"go.uber.org/zap/zaptest"
|
||||
|
||||
telegramloginapp "telesrv/internal/app/telegramlogin"
|
||||
appusers "telesrv/internal/app/users"
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store/memory"
|
||||
)
|
||||
|
||||
type telegramLoginBotPermissionAdapter struct{ bots BotsService }
|
||||
|
||||
func (a telegramLoginBotPermissionAdapter) AllowBotSendMessage(ctx context.Context, botUserID, userID int64, fromRequest bool) (bool, error) {
|
||||
return a.bots.AllowSendMessage(ctx, userID, botUserID, fromRequest)
|
||||
}
|
||||
|
||||
type telegramLoginRPCFixture struct {
|
||||
ctx context.Context
|
||||
service *telegramloginapp.Service
|
||||
router *Router
|
||||
user domain.User
|
||||
intruder domain.User
|
||||
bot domain.User
|
||||
client telegramloginapp.ClientCredentials
|
||||
redirect string
|
||||
}
|
||||
|
||||
func newTelegramLoginRPCFixture(t *testing.T) *telegramLoginRPCFixture {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
users := memory.NewUserStore()
|
||||
user, err := users.Create(ctx, domain.User{Phone: "+15551001", FirstName: "Alice", LastName: "Example", Username: "alice", AccessHash: 11})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
intruder, err := users.Create(ctx, domain.User{Phone: "+15551002", FirstName: "Mallory", Username: "mallory", AccessHash: 13})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bot, err := users.Create(ctx, domain.User{FirstName: "Login Bot", Username: "login_rpc_bot", AccessHash: 12, Bot: true, BotInfoVersion: 1})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sealKey := make([]byte, 32)
|
||||
sealKey[0] = 3
|
||||
sealer, err := telegramloginapp.NewCodeSealer("test", map[string][]byte{"test": sealKey})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pepper := make([]byte, 32)
|
||||
pepper[0] = 4
|
||||
service, err := telegramloginapp.NewService(memory.NewTelegramLoginStore(nil), sealer, telegramloginapp.Config{
|
||||
Issuer: "https://oauth.test", AppScheme: "telesrv", ClientSecretPepper: pepper,
|
||||
Now: func() time.Time { return time.Unix(1_780_000_000, 0).UTC() },
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
client, err := service.CreateClient(ctx, bot.ID, domain.TelegramLoginSigningRS256)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
redirect := "https://rp.test/callback"
|
||||
if _, err := service.AddAllowedURL(ctx, bot.ID, domain.TelegramLoginAllowedRedirectURI, redirect); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := service.AddAllowedURL(ctx, bot.ID, domain.TelegramLoginAllowedWebOrigin, "https://rp.test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
router := New(Config{}, Deps{Users: appusers.NewService(users), TelegramLogin: service}, zaptest.NewLogger(t), clock.System)
|
||||
return &telegramLoginRPCFixture{ctx: WithUserID(ctx, user.ID), service: service, router: router, user: user, intruder: intruder, bot: bot, client: client, redirect: redirect}
|
||||
}
|
||||
|
||||
func (f *telegramLoginRPCFixture) authorization(t *testing.T, match bool) telegramloginapp.CreatedAuthorization {
|
||||
t.Helper()
|
||||
challenge, err := telegramloginapp.PKCEChallenge("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
created, err := f.service.CreateAuthorization(f.ctx, telegramloginapp.CreateAuthorizationParams{
|
||||
ClientID: f.client.Client.ClientID, RedirectURI: f.redirect, ResponseType: "code",
|
||||
Scope: "openid profile telegram:bot_access", CodeChallenge: challenge, CodeChallengeMethod: "S256",
|
||||
IncludeMatchCodes: match, MatchCodesFirst: match,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return created
|
||||
}
|
||||
|
||||
func TestTelegramLoginRPCsAcrossExactLayerProfiles(t *testing.T) {
|
||||
for profile := tlprofile.Profile225; profile <= tlprofile.Profile228; profile++ {
|
||||
t.Run(fmt.Sprintf("layer_%d", profile), func(t *testing.T) {
|
||||
f := newTelegramLoginRPCFixture(t)
|
||||
|
||||
approve := f.authorization(t, true)
|
||||
// TDesktop normalizes the configured telesrv:// launcher to the
|
||||
// official internal tg://oauth form before invoking MTProto.
|
||||
canonicalURL := strings.Replace(approve.DeepLink, "telesrv://", "tg://", 1)
|
||||
request := &tg.MessagesRequestURLAuthRequest{}
|
||||
request.SetURL(canonicalURL)
|
||||
result, method := dispatchExactLayerRPCTest(t, f.router, f.ctx, profile, request)
|
||||
if method != "messages.requestUrlAuth" {
|
||||
t.Fatalf("method = %q", method)
|
||||
}
|
||||
prompt, ok := dispatchCanonicalValue(result).(*tg.URLAuthResultRequest)
|
||||
if !ok || prompt.Bot.GetID() != f.bot.ID || !prompt.RequestWriteAccess || len(prompt.MatchCodes) != 5 {
|
||||
t.Fatalf("request result = %#v", dispatchCanonicalValue(result))
|
||||
}
|
||||
|
||||
checked, method := dispatchExactLayerRPCTest(t, f.router, f.ctx, profile, &tg.MessagesCheckURLAuthMatchCodeRequest{
|
||||
URL: canonicalURL, MatchCode: approve.Request.MatchCode,
|
||||
})
|
||||
if method != "messages.checkUrlAuthMatchCode" || dispatchCanonicalValue(checked) != true {
|
||||
t.Fatalf("check result = %#v method=%q", dispatchCanonicalValue(checked), method)
|
||||
}
|
||||
accept := &tg.MessagesAcceptURLAuthRequest{WriteAllowed: true}
|
||||
accept.SetURL(canonicalURL)
|
||||
accept.SetMatchCode(approve.Request.MatchCode)
|
||||
accepted, method := dispatchExactLayerRPCTest(t, f.router, f.ctx, profile, accept)
|
||||
if method != "messages.acceptUrlAuth" {
|
||||
t.Fatalf("accept method = %q", method)
|
||||
}
|
||||
if _, ok := dispatchCanonicalValue(accepted).(*tg.URLAuthResultAccepted); !ok {
|
||||
t.Fatalf("accept result = %#v", dispatchCanonicalValue(accepted))
|
||||
}
|
||||
|
||||
decline := f.authorization(t, false)
|
||||
declined, method := dispatchExactLayerRPCTest(t, f.router, f.ctx, profile, &tg.MessagesDeclineURLAuthRequest{URL: decline.DeepLink})
|
||||
if method != "messages.declineUrlAuth" || dispatchCanonicalValue(declined) != true {
|
||||
t.Fatalf("decline result = %#v method=%q", dispatchCanonicalValue(declined), method)
|
||||
}
|
||||
|
||||
listed, method := dispatchExactLayerRPCTest(t, f.router, f.ctx, profile, &tg.AccountGetWebAuthorizationsRequest{})
|
||||
web, ok := dispatchCanonicalValue(listed).(*tg.AccountWebAuthorizations)
|
||||
if method != "account.getWebAuthorizations" || !ok || len(web.Authorizations) != 1 || web.Authorizations[0].BotID != f.bot.ID {
|
||||
t.Fatalf("getWebAuthorizations = %#v method=%q", dispatchCanonicalValue(listed), method)
|
||||
}
|
||||
reset, method := dispatchExactLayerRPCTest(t, f.router, f.ctx, profile, &tg.AccountResetWebAuthorizationRequest{Hash: web.Authorizations[0].Hash})
|
||||
if method != "account.resetWebAuthorization" || dispatchCanonicalValue(reset) != true {
|
||||
t.Fatalf("resetWebAuthorization = %#v method=%q", dispatchCanonicalValue(reset), method)
|
||||
}
|
||||
|
||||
const nativeCallback = "bedolaga://telegram-login"
|
||||
if _, err := f.service.AddNativeApp(f.ctx, f.bot.ID, domain.TelegramLoginNativeAndroid,
|
||||
"dev.bedolaga.demo", strings.Repeat("A", 64), nativeCallback, "Bedolaga Android Demo"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
challenge, err := telegramloginapp.PKCEChallenge("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
native, err := f.service.CreateAuthorization(f.ctx, telegramloginapp.CreateAuthorizationParams{
|
||||
ClientID: f.client.Client.ClientID, RedirectURI: nativeCallback, ResponseType: "code",
|
||||
Scope: "profile", CodeChallenge: challenge, CodeChallengeMethod: "S256",
|
||||
NativePlatform: domain.TelegramLoginNativeAndroid, IncludeMatchCodes: true, MatchCodesFirst: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
nativeRequest := &tg.MessagesRequestURLAuthRequest{}
|
||||
nativeRequest.SetURL(native.DeepLink)
|
||||
nativeResult, _ := dispatchExactLayerRPCTest(t, f.router, f.ctx, profile, nativeRequest)
|
||||
nativePrompt, ok := dispatchCanonicalValue(nativeResult).(*tg.URLAuthResultRequest)
|
||||
if !ok || !nativePrompt.IsApp || nativePrompt.VerifiedAppName != "Bedolaga Android Demo" || len(nativePrompt.MatchCodes) != 5 {
|
||||
t.Fatalf("native request result = %#v", dispatchCanonicalValue(nativeResult))
|
||||
}
|
||||
nativeAccept := &tg.MessagesAcceptURLAuthRequest{}
|
||||
nativeAccept.SetURL(native.DeepLink)
|
||||
nativeAccept.SetMatchCode(native.Request.MatchCode)
|
||||
nativeAcceptedResult, _ := dispatchExactLayerRPCTest(t, f.router, f.ctx, profile, nativeAccept)
|
||||
nativeAccepted, ok := dispatchCanonicalValue(nativeAcceptedResult).(*tg.URLAuthResultAccepted)
|
||||
if !ok || !strings.HasPrefix(nativeAccepted.URL, nativeCallback+"?code=") {
|
||||
t.Fatalf("native accepted result = %#v", dispatchCanonicalValue(nativeAcceptedResult))
|
||||
}
|
||||
nativeRetryResult, _ := dispatchExactLayerRPCTest(t, f.router, f.ctx, profile, nativeRequest)
|
||||
nativeRetry, ok := dispatchCanonicalValue(nativeRetryResult).(*tg.URLAuthResultAccepted)
|
||||
if !ok || nativeRetry.URL != nativeAccepted.URL {
|
||||
t.Fatalf("native retry result = %#v, want URL %q", dispatchCanonicalValue(nativeRetryResult), nativeAccepted.URL)
|
||||
}
|
||||
|
||||
const miniAppOrigin = "https://rp.test"
|
||||
miniApp, err := f.service.CreateAuthorization(f.ctx, telegramloginapp.CreateAuthorizationParams{
|
||||
ClientID: f.client.Client.ClientID, RedirectURI: miniAppOrigin + "/", ResponseType: "post_message",
|
||||
Scope: "openid profile", Origin: miniAppOrigin, InAppOrigin: miniAppOrigin,
|
||||
Source: domain.TelegramLoginRequestMiniApp, IncludeMatchCodes: true, MatchCodesFirst: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
miniAppRequest := &tg.MessagesRequestURLAuthRequest{}
|
||||
miniAppRequest.SetURL(miniApp.DeepLink)
|
||||
miniAppRequest.SetInAppOrigin(miniAppOrigin)
|
||||
miniAppResult, method := dispatchExactLayerRPCTest(t, f.router, f.ctx, profile, miniAppRequest)
|
||||
miniAppPrompt, ok := dispatchCanonicalValue(miniAppResult).(*tg.URLAuthResultRequest)
|
||||
if method != "messages.requestUrlAuth" || !ok || len(miniAppPrompt.MatchCodes) != 5 {
|
||||
t.Fatalf("mini-app request result = %#v method=%q", dispatchCanonicalValue(miniAppResult), method)
|
||||
}
|
||||
miniAppAccept := &tg.MessagesAcceptURLAuthRequest{}
|
||||
miniAppAccept.SetURL(miniApp.DeepLink)
|
||||
miniAppAccept.SetMatchCode(miniApp.Request.MatchCode)
|
||||
miniAppAcceptedResult, method := dispatchExactLayerRPCTest(t, f.router, f.ctx, profile, miniAppAccept)
|
||||
miniAppAccepted, ok := dispatchCanonicalValue(miniAppAcceptedResult).(*tg.URLAuthResultAccepted)
|
||||
if method != "messages.acceptUrlAuth" || !ok || !strings.HasPrefix(miniAppAccepted.URL, "https://oauth.test/inapp?token=") {
|
||||
t.Fatalf("mini-app accepted result = %#v method=%q", dispatchCanonicalValue(miniAppAcceptedResult), method)
|
||||
}
|
||||
miniAppRetryResult, _ := dispatchExactLayerRPCTest(t, f.router, f.ctx, profile, miniAppRequest)
|
||||
miniAppRetry, ok := dispatchCanonicalValue(miniAppRetryResult).(*tg.URLAuthResultAccepted)
|
||||
if !ok || miniAppRetry.URL != miniAppAccepted.URL {
|
||||
t.Fatalf("mini-app retry result = %#v, want URL %q", dispatchCanonicalValue(miniAppRetryResult), miniAppAccepted.URL)
|
||||
}
|
||||
resetAll, method := dispatchExactLayerRPCTest(t, f.router, f.ctx, profile, &tg.AccountResetWebAuthorizationsRequest{})
|
||||
if method != "account.resetWebAuthorizations" || dispatchCanonicalValue(resetAll) != true {
|
||||
t.Fatalf("resetWebAuthorizations = %#v method=%q", dispatchCanonicalValue(resetAll), method)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTelegramLoginApprovedDeepLinkRejectsAnotherUser(t *testing.T) {
|
||||
f := newTelegramLoginRPCFixture(t)
|
||||
created := f.authorization(t, false)
|
||||
accept := &tg.MessagesAcceptURLAuthRequest{}
|
||||
accept.SetURL(created.DeepLink)
|
||||
if _, err := f.router.onMessagesAcceptURLAuth(f.ctx, accept); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request := &tg.MessagesRequestURLAuthRequest{}
|
||||
request.SetURL(created.DeepLink)
|
||||
if _, err := f.router.onMessagesRequestURLAuth(WithUserID(context.Background(), f.intruder.ID), request); err == nil {
|
||||
t.Fatal("another user observed an approved deep link as accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTelegramLoginMessageButtonRereadSignsAndGrantsWriteAccess(t *testing.T) {
|
||||
f := newBotAPIReceiveFixture(t, false)
|
||||
sealKey := make([]byte, 32)
|
||||
sealKey[0] = 7
|
||||
sealer, err := telegramloginapp.NewCodeSealer("test", map[string][]byte{"test": sealKey})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pepper := make([]byte, 32)
|
||||
pepper[0] = 8
|
||||
loginStore := memory.NewTelegramLoginStore(telegramLoginBotPermissionAdapter{bots: f.router.deps.Bots})
|
||||
login, err := telegramloginapp.NewService(loginStore, sealer, telegramloginapp.Config{
|
||||
Issuer: "https://oauth.test", AppScheme: "telesrv", ClientSecretPepper: pepper,
|
||||
Now: func() time.Time { return time.Unix(1_780_000_000, 0).UTC() },
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := login.CreateClient(f.ctx, f.bot.ID, domain.TelegramLoginSigningRS256); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := login.AddAllowedURL(f.ctx, f.bot.ID, domain.TelegramLoginAllowedWebOrigin, "https://rp.test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.router.deps.TelegramLogin = login
|
||||
|
||||
markup := &domain.MessageReplyMarkup{Type: domain.MessageReplyMarkupInline, Inline: [][]domain.MarkupButton{{{
|
||||
Type: domain.MarkupButtonLoginURL, Text: "Log in", URL: "https://rp.test/login?next=%2Fhome", RequestWriteAccess: true,
|
||||
}}}}
|
||||
if _, err := f.router.BotAPISendMessage(f.ctx, f.bot.ID, f.owner.ID, "Authorize", nil, markup, false, false, 0); err != nil {
|
||||
t.Fatalf("BotAPISendMessage: %v", err)
|
||||
}
|
||||
history, err := f.messages.GetHistory(f.ctx, f.owner.ID, domain.MessageFilter{
|
||||
HasPeer: true, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: f.bot.ID}, Limit: 10,
|
||||
})
|
||||
if err != nil || len(history.Messages) == 0 {
|
||||
t.Fatalf("GetHistory: messages=%d err=%v", len(history.Messages), err)
|
||||
}
|
||||
message := history.Messages[0]
|
||||
if message.ReplyMarkup == nil || message.ReplyMarkup.Inline[0][0].LoginBotUserID != f.bot.ID {
|
||||
t.Fatalf("persisted login button = %#v", message.ReplyMarkup)
|
||||
}
|
||||
|
||||
peer := &tg.InputPeerUser{UserID: f.bot.ID, AccessHash: f.bot.AccessHash}
|
||||
request := &tg.MessagesRequestURLAuthRequest{}
|
||||
request.SetPeer(peer)
|
||||
request.SetMsgID(message.ID)
|
||||
request.SetButtonID(0)
|
||||
requested, err := f.router.onMessagesRequestURLAuth(WithUserID(f.ctx, f.owner.ID), request)
|
||||
if err != nil {
|
||||
t.Fatalf("requestUrlAuth: %v", err)
|
||||
}
|
||||
prompt, ok := requested.(*tg.URLAuthResultRequest)
|
||||
if !ok || !prompt.RequestWriteAccess || prompt.Domain != "rp.test" {
|
||||
t.Fatalf("requestUrlAuth result = %#v", requested)
|
||||
}
|
||||
accept := &tg.MessagesAcceptURLAuthRequest{}
|
||||
accept.SetWriteAllowed(true)
|
||||
accept.SetPeer(peer)
|
||||
accept.SetMsgID(message.ID)
|
||||
accept.SetButtonID(0)
|
||||
accepted, err := f.router.onMessagesAcceptURLAuth(WithUserID(f.ctx, f.owner.ID), accept)
|
||||
if err != nil {
|
||||
t.Fatalf("acceptUrlAuth: %v", err)
|
||||
}
|
||||
final, ok := accepted.(*tg.URLAuthResultAccepted)
|
||||
if !ok || final.URL == "" {
|
||||
t.Fatalf("acceptUrlAuth result = %#v", accepted)
|
||||
}
|
||||
verifyLegacyTelegramLoginURL(t, final.URL, domain.FormatBotToken(f.bot.ID, "secret"), f.owner.ID)
|
||||
if allowed, err := f.router.deps.Bots.CanSendMessage(f.ctx, f.owner.ID, f.bot.ID); err != nil || !allowed {
|
||||
t.Fatalf("bot write permission = %v,%v", allowed, err)
|
||||
}
|
||||
web, err := login.ListWebAuthorizations(f.ctx, f.owner.ID)
|
||||
if err != nil || len(web) != 1 || !web[0].BotAccessGranted || web[0].Domain != "rp.test" {
|
||||
t.Fatalf("web authorizations = %#v err=%v", web, err)
|
||||
}
|
||||
|
||||
// The server must re-read durable message state. A forged button id never
|
||||
// falls back to URL data supplied by the client.
|
||||
forged := &tg.MessagesAcceptURLAuthRequest{}
|
||||
forged.SetPeer(peer)
|
||||
forged.SetMsgID(message.ID)
|
||||
forged.SetButtonID(99)
|
||||
if _, err := f.router.onMessagesAcceptURLAuth(WithUserID(f.ctx, f.owner.ID), forged); err == nil {
|
||||
t.Fatal("forged button id was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTelegramLoginMarkupRequiresBotSender(t *testing.T) {
|
||||
f := newTelegramLoginRPCFixture(t)
|
||||
markup := &domain.MessageReplyMarkup{Type: domain.MessageReplyMarkupInline, Inline: [][]domain.MarkupButton{{{
|
||||
Type: domain.MarkupButtonLoginURL, Text: "Log in", URL: "https://rp.test/login",
|
||||
}}}}
|
||||
if err := f.router.prepareTelegramLoginMarkup(WithUserID(f.ctx, f.user.ID), f.user.ID, markup); !errors.Is(err, domain.ErrButtonTypeInvalid) {
|
||||
t.Fatalf("ordinary user login_url error = %v, want ErrButtonTypeInvalid", err)
|
||||
}
|
||||
}
|
||||
|
||||
func verifyLegacyTelegramLoginURL(t *testing.T, raw, botToken string, wantUserID int64) {
|
||||
t.Helper()
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
query := u.Query()
|
||||
provided := query.Get("hash")
|
||||
query.Del("hash")
|
||||
if query.Get("id") != strconv.FormatInt(wantUserID, 10) || query.Get("auth_date") == "" || query.Get("next") != "/home" {
|
||||
t.Fatalf("legacy login query = %#v", query)
|
||||
}
|
||||
keys := make([]string, 0, len(query))
|
||||
for key := range query {
|
||||
if key != "next" { // Existing application query fields are not signed.
|
||||
keys = append(keys, key)
|
||||
}
|
||||
}
|
||||
sort.Strings(keys)
|
||||
lines := make([]string, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
lines = append(lines, key+"="+query.Get(key))
|
||||
}
|
||||
secret := sha256.Sum256([]byte(botToken))
|
||||
mac := hmac.New(sha256.New, secret[:])
|
||||
_, _ = mac.Write([]byte(strings.Join(lines, "\n")))
|
||||
if !hmac.Equal([]byte(strings.ToLower(provided)), []byte(hex.EncodeToString(mac.Sum(nil)))) {
|
||||
t.Fatalf("legacy login hash = %q, want %s", provided, hex.EncodeToString(mac.Sum(nil)))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue