320 lines
12 KiB
Go
320 lines
12 KiB
Go
package memory
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"telesrv/internal/domain"
|
|
)
|
|
|
|
type telegramLoginPermissionRecorder struct {
|
|
mu sync.Mutex
|
|
grants map[[2]int64]int
|
|
}
|
|
|
|
func (r *telegramLoginPermissionRecorder) AllowBotSendMessage(_ context.Context, botUserID, userID int64, _ bool) (bool, error) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if r.grants == nil {
|
|
r.grants = make(map[[2]int64]int)
|
|
}
|
|
key := [2]int64{botUserID, userID}
|
|
created := r.grants[key] == 0
|
|
r.grants[key]++
|
|
return created, nil
|
|
}
|
|
|
|
func telegramLoginTestHash(value string) []byte {
|
|
sum := sha256.Sum256([]byte(value))
|
|
return sum[:]
|
|
}
|
|
|
|
func seedTelegramLoginRequest(t *testing.T, s *TelegramLoginStore, now time.Time) domain.TelegramLoginRequest {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
client := domain.TelegramLoginClient{
|
|
BotUserID: 9001,
|
|
ClientID: "9001",
|
|
SecretHash: telegramLoginTestHash("client-secret"),
|
|
SecretVersion: 1,
|
|
SigningAlgorithm: domain.TelegramLoginSigningRS256,
|
|
Enabled: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
if _, err := s.UpsertTelegramLoginClient(ctx, client); err != nil {
|
|
t.Fatalf("UpsertTelegramLoginClient: %v", err)
|
|
}
|
|
if _, err := s.AddTelegramLoginAllowedURL(ctx, domain.TelegramLoginAllowedURL{
|
|
BotUserID: client.BotUserID, Kind: domain.TelegramLoginAllowedRedirectURI,
|
|
NormalizedURL: "https://rp.example/callback", CreatedAt: now,
|
|
}); err != nil {
|
|
t.Fatalf("AddTelegramLoginAllowedURL: %v", err)
|
|
}
|
|
request := domain.TelegramLoginRequest{
|
|
RequestTokenHash: telegramLoginTestHash("request-token"),
|
|
BrowserTokenHash: telegramLoginTestHash("browser-token"),
|
|
BotUserID: client.BotUserID,
|
|
ClientID: client.ClientID,
|
|
SigningAlgorithm: client.SigningAlgorithm,
|
|
Source: domain.TelegramLoginRequestWeb,
|
|
ResponseType: "code",
|
|
RedirectURI: "https://rp.example/callback",
|
|
Origin: "https://rp.example",
|
|
Domain: "rp.example",
|
|
Scopes: []domain.TelegramLoginScope{domain.TelegramLoginScopeOpenID, domain.TelegramLoginScopeProfile, domain.TelegramLoginScopePhone, domain.TelegramLoginScopeBotAccess},
|
|
State: "state",
|
|
Nonce: "nonce",
|
|
CodeChallenge: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
CodeChallengeMethod: "S256",
|
|
Browser: "Firefox",
|
|
Platform: "Windows",
|
|
IP: "192.0.2.10",
|
|
Region: "Test Region",
|
|
MatchCodes: []string{"🟢", "🔵", "🟠"},
|
|
MatchCode: "🔵",
|
|
MatchCodesFirst: true,
|
|
Status: domain.TelegramLoginRequestPending,
|
|
CreatedAt: now,
|
|
ExpiresAt: now.Add(5 * time.Minute),
|
|
}
|
|
created, err := s.CreateTelegramLoginRequest(ctx, request)
|
|
if err != nil {
|
|
t.Fatalf("CreateTelegramLoginRequest: %v", err)
|
|
}
|
|
return created
|
|
}
|
|
|
|
func approveTelegramLoginRequest(t *testing.T, s *TelegramLoginStore, request domain.TelegramLoginRequest, now time.Time) (domain.TelegramLoginRequest, domain.TelegramLoginWebAuthorization) {
|
|
t.Helper()
|
|
approved, web, err := s.ApproveTelegramLoginRequest(context.Background(), domain.TelegramLoginApproval{
|
|
RequestID: request.ID,
|
|
Identity: domain.TelegramLoginIdentitySnapshot{
|
|
UserID: 42, Name: "Alice Example", GivenName: "Alice", FamilyName: "Example",
|
|
PreferredUsername: "alice", Picture: "https://oauth.example/userpic/42",
|
|
},
|
|
WriteAllowed: true,
|
|
PhoneShared: false,
|
|
MatchCode: request.MatchCode,
|
|
ApprovedAt: now,
|
|
}, 7000+request.ID)
|
|
if err != nil {
|
|
t.Fatalf("ApproveTelegramLoginRequest: %v", err)
|
|
}
|
|
return approved, web
|
|
}
|
|
|
|
func TestTelegramLoginApproveIsAtomicAndShrinksConsent(t *testing.T) {
|
|
now := time.Unix(1_780_000_000, 0)
|
|
permissions := &telegramLoginPermissionRecorder{}
|
|
s := NewTelegramLoginStore(permissions)
|
|
request := seedTelegramLoginRequest(t, s, now)
|
|
approved, web := approveTelegramLoginRequest(t, s, request, now.Add(time.Second))
|
|
if approved.Status != domain.TelegramLoginRequestApproved || approved.AuthorizedUserID != 42 {
|
|
t.Fatalf("approved request = %#v", approved)
|
|
}
|
|
if web.PhoneShared || web.BotAccessGranted != true {
|
|
t.Fatalf("web consent = %#v", web)
|
|
}
|
|
if len(web.Scopes) != 3 || web.Scopes[0] != domain.TelegramLoginScopeOpenID || web.Scopes[1] != domain.TelegramLoginScopeProfile || web.Scopes[2] != domain.TelegramLoginScopeBotAccess {
|
|
t.Fatalf("granted scopes = %#v", web.Scopes)
|
|
}
|
|
permissions.mu.Lock()
|
|
grants := permissions.grants[[2]int64{9001, 42}]
|
|
permissions.mu.Unlock()
|
|
if grants != 1 {
|
|
t.Fatalf("bot permission grants = %d, want 1", grants)
|
|
}
|
|
}
|
|
|
|
func TestTelegramLoginAcceptDeclineRaceHasOneTerminalState(t *testing.T) {
|
|
now := time.Unix(1_780_000_000, 0)
|
|
s := NewTelegramLoginStore(nil)
|
|
request := seedTelegramLoginRequest(t, s, now)
|
|
start := make(chan struct{})
|
|
errs := make(chan error, 2)
|
|
go func() {
|
|
<-start
|
|
_, _, err := s.ApproveTelegramLoginRequest(context.Background(), domain.TelegramLoginApproval{
|
|
RequestID: request.ID,
|
|
Identity: domain.TelegramLoginIdentitySnapshot{UserID: 42, Name: "Alice", GivenName: "Alice"},
|
|
MatchCode: request.MatchCode, ApprovedAt: now.Add(time.Second),
|
|
}, 7001)
|
|
errs <- err
|
|
}()
|
|
go func() {
|
|
<-start
|
|
_, err := s.DeclineTelegramLoginRequest(context.Background(), request.ID, 42, now.Add(time.Second))
|
|
errs <- err
|
|
}()
|
|
close(start)
|
|
var success, conflict int
|
|
for range 2 {
|
|
err := <-errs
|
|
switch {
|
|
case err == nil:
|
|
success++
|
|
case errors.Is(err, domain.ErrTelegramLoginRequestConflict):
|
|
conflict++
|
|
default:
|
|
t.Fatalf("unexpected race error: %v", err)
|
|
}
|
|
}
|
|
if success != 1 || conflict != 1 {
|
|
t.Fatalf("success=%d conflict=%d, want 1/1", success, conflict)
|
|
}
|
|
}
|
|
|
|
func TestTelegramLoginAuthorizationCodeSingleConsumeAndRevocation(t *testing.T) {
|
|
now := time.Unix(1_780_000_000, 0)
|
|
s := NewTelegramLoginStore(nil)
|
|
request := seedTelegramLoginRequest(t, s, now)
|
|
approveTelegramLoginRequest(t, s, request, now.Add(time.Second))
|
|
code := domain.TelegramLoginAuthorizationCode{
|
|
RequestID: request.ID,
|
|
CodeHash: telegramLoginTestHash("authorization-code"),
|
|
SealedCode: append(make([]byte, 32), 1),
|
|
SealNonce: make([]byte, 12),
|
|
SealKeyID: "test-key",
|
|
IssuedAt: now.Add(2 * time.Second),
|
|
ExpiresAt: now.Add(time.Minute),
|
|
}
|
|
if _, err := s.PutTelegramLoginAuthorizationCode(context.Background(), code); err != nil {
|
|
t.Fatalf("PutTelegramLoginAuthorizationCode: %v", err)
|
|
}
|
|
|
|
start := make(chan struct{})
|
|
errs := make(chan error, 8)
|
|
for range 8 {
|
|
go func() {
|
|
<-start
|
|
_, _, _, err := s.ConsumeTelegramLoginAuthorizationCode(context.Background(), domain.TelegramLoginCodeExchange{
|
|
CodeHash: code.CodeHash, ClientID: request.ClientID, ClientSecretVersion: 1,
|
|
RedirectURI: request.RedirectURI, CodeChallenge: request.CodeChallenge, Now: now.Add(3 * time.Second),
|
|
})
|
|
errs <- err
|
|
}()
|
|
}
|
|
close(start)
|
|
var success, consumed int
|
|
for range 8 {
|
|
err := <-errs
|
|
switch {
|
|
case err == nil:
|
|
success++
|
|
case errors.Is(err, domain.ErrTelegramLoginCodeConsumed):
|
|
consumed++
|
|
default:
|
|
t.Fatalf("unexpected consume error: %v", err)
|
|
}
|
|
}
|
|
if success != 1 || consumed != 7 {
|
|
t.Fatalf("success=%d consumed=%d, want 1/7", success, consumed)
|
|
}
|
|
|
|
request2 := request.Clone()
|
|
request2.ID = 0
|
|
request2.RequestTokenHash = telegramLoginTestHash("request-token-2")
|
|
request2.BrowserTokenHash = telegramLoginTestHash("browser-token-2")
|
|
request2, err := s.CreateTelegramLoginRequest(context.Background(), request2)
|
|
if err != nil {
|
|
t.Fatalf("Create second request: %v", err)
|
|
}
|
|
_, web2 := approveTelegramLoginRequest(t, s, request2, now.Add(4*time.Second))
|
|
code2 := code.Clone()
|
|
code2.ID = 0
|
|
code2.RequestID = request2.ID
|
|
code2.CodeHash = telegramLoginTestHash("authorization-code-2")
|
|
if _, err := s.PutTelegramLoginAuthorizationCode(context.Background(), code2); err != nil {
|
|
t.Fatalf("Put second code: %v", err)
|
|
}
|
|
if revoked, err := s.RevokeTelegramLoginWebAuthorization(context.Background(), web2.UserID, web2.Hash, now.Add(5*time.Second)); err != nil || !revoked {
|
|
t.Fatalf("RevokeTelegramLoginWebAuthorization = %v,%v", revoked, err)
|
|
}
|
|
if _, _, _, err := s.ConsumeTelegramLoginAuthorizationCode(context.Background(), domain.TelegramLoginCodeExchange{
|
|
CodeHash: code2.CodeHash, ClientID: request2.ClientID, ClientSecretVersion: 1,
|
|
RedirectURI: request2.RedirectURI, CodeChallenge: request2.CodeChallenge, Now: now.Add(6 * time.Second),
|
|
}); !errors.Is(err, domain.ErrTelegramLoginCodeInvalid) {
|
|
t.Fatalf("consume after revoke error = %v, want code invalid", err)
|
|
}
|
|
}
|
|
|
|
func TestTelegramLoginRetentionPreservesActiveAndReferencedApprovals(t *testing.T) {
|
|
ctx := context.Background()
|
|
now := time.Unix(1_780_000_000, 0)
|
|
before := now.Add(24 * time.Hour)
|
|
s := NewTelegramLoginStore(nil)
|
|
|
|
active := seedTelegramLoginRequest(t, s, now)
|
|
_, activeWeb := approveTelegramLoginRequest(t, s, active, now.Add(time.Second))
|
|
|
|
revoked := active.Clone()
|
|
revoked.ID = 0
|
|
revoked.RequestTokenHash = telegramLoginTestHash("retention-revoked-request")
|
|
revoked.BrowserTokenHash = telegramLoginTestHash("retention-revoked-browser")
|
|
revoked.Status = domain.TelegramLoginRequestPending
|
|
revoked.AuthorizedUserID = 0
|
|
revoked.ProfileName, revoked.GivenName, revoked.FamilyName = "", "", ""
|
|
revoked.PreferredUsername, revoked.Picture, revoked.PhoneNumber = "", "", ""
|
|
revoked.WriteAllowed, revoked.PhoneShared = false, false
|
|
revoked.ApprovedAt = time.Time{}
|
|
revoked, err := s.CreateTelegramLoginRequest(ctx, revoked)
|
|
if err != nil {
|
|
t.Fatalf("create revoked request: %v", err)
|
|
}
|
|
_, revokedWeb := approveTelegramLoginRequest(t, s, revoked, now.Add(2*time.Second))
|
|
if ok, err := s.RevokeTelegramLoginWebAuthorization(ctx, revokedWeb.UserID, revokedWeb.Hash, now.Add(3*time.Second)); err != nil || !ok {
|
|
t.Fatalf("revoke old authorization = %v,%v", ok, err)
|
|
}
|
|
|
|
referenced := revoked.Clone()
|
|
referenced.ID = 0
|
|
referenced.RequestTokenHash = telegramLoginTestHash("retention-referenced-request")
|
|
referenced.BrowserTokenHash = telegramLoginTestHash("retention-referenced-browser")
|
|
referenced.Status = domain.TelegramLoginRequestPending
|
|
referenced.AuthorizedUserID = 0
|
|
referenced.ProfileName, referenced.GivenName, referenced.FamilyName = "", "", ""
|
|
referenced.PreferredUsername, referenced.Picture, referenced.PhoneNumber = "", "", ""
|
|
referenced.WriteAllowed, referenced.PhoneShared = false, false
|
|
referenced.ApprovedAt = time.Time{}
|
|
referenced, err = s.CreateTelegramLoginRequest(ctx, referenced)
|
|
if err != nil {
|
|
t.Fatalf("create referenced request: %v", err)
|
|
}
|
|
_, referencedWeb := approveTelegramLoginRequest(t, s, referenced, now.Add(4*time.Second))
|
|
if _, err := s.PutTelegramLoginAuthorizationCode(ctx, domain.TelegramLoginAuthorizationCode{
|
|
RequestID: referenced.ID, CodeHash: telegramLoginTestHash("retention-live-code"),
|
|
SealedCode: append(make([]byte, 32), 1), SealNonce: make([]byte, 12), SealKeyID: "test-key",
|
|
IssuedAt: before.Add(time.Hour), ExpiresAt: before.Add(2 * time.Hour),
|
|
}); err != nil {
|
|
t.Fatalf("put retained code: %v", err)
|
|
}
|
|
if ok, err := s.RevokeTelegramLoginWebAuthorization(ctx, referencedWeb.UserID, referencedWeb.Hash, now.Add(5*time.Second)); err != nil || !ok {
|
|
t.Fatalf("revoke referenced authorization = %v,%v", ok, err)
|
|
}
|
|
|
|
deleted, err := s.DeleteExpiredTelegramLoginArtifacts(ctx, before, 100)
|
|
if err != nil {
|
|
t.Fatalf("delete expired artifacts: %v", err)
|
|
}
|
|
if deleted != 1 {
|
|
t.Fatalf("deleted = %d, want revoked request only", deleted)
|
|
}
|
|
if _, found, _ := s.GetTelegramLoginRequest(ctx, active.ID); !found {
|
|
t.Fatal("active authorization request was deleted")
|
|
}
|
|
if _, found, _ := s.GetTelegramLoginRequest(ctx, referenced.ID); !found {
|
|
t.Fatal("request with retained code was deleted")
|
|
}
|
|
if _, found, _ := s.GetTelegramLoginRequest(ctx, revoked.ID); found {
|
|
t.Fatal("old revoked authorization request was retained")
|
|
}
|
|
listed, err := s.ListTelegramLoginWebAuthorizations(ctx, activeWeb.UserID)
|
|
if err != nil || len(listed) != 1 || listed[0].Hash != activeWeb.Hash {
|
|
t.Fatalf("active authorizations after retention = %#v, %v", listed, err)
|
|
}
|
|
}
|