218 lines
6.8 KiB
Go
218 lines
6.8 KiB
Go
package token
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/tinode/chat/server/auth"
|
|
"github.com/tinode/chat/server/store"
|
|
"github.com/tinode/chat/server/store/types"
|
|
)
|
|
|
|
type testSecurityStore struct {
|
|
version int
|
|
err error
|
|
}
|
|
|
|
func (s testSecurityStore) GetAuthVersion(types.TenantID, types.Uid) (int, error) {
|
|
return s.version, s.err
|
|
}
|
|
|
|
func (s testSecurityStore) BumpAuthVersion(types.TenantID, types.Uid, time.Time) (int, error) {
|
|
return s.version + 1, s.err
|
|
}
|
|
|
|
func (s testSecurityStore) GetRealName(types.TenantID, types.Uid) (*types.RealNameVerification, error) {
|
|
return nil, types.ErrNotFound
|
|
}
|
|
|
|
func (s testSecurityStore) CreateRealNameSubmission(types.TenantID, *types.RealNameVerification) error {
|
|
return s.err
|
|
}
|
|
|
|
func (s testSecurityStore) CreateRealNameAppeal(types.TenantID, *types.RealNameAppeal) (*types.RealNameAppeal, error) {
|
|
return nil, s.err
|
|
}
|
|
|
|
func (s testSecurityStore) CreateActionToken(types.TenantID, *types.ActionToken) error {
|
|
return s.err
|
|
}
|
|
|
|
func (s testSecurityStore) ConsumeActionToken(types.TenantID, types.Uid, string, string, string, time.Time) (*types.ActionToken, error) {
|
|
return nil, s.err
|
|
}
|
|
|
|
func (s testSecurityStore) GetIndependentPassword(types.TenantID, types.Uid, string) (*types.IndependentPassword, error) {
|
|
return nil, types.ErrNotFound
|
|
}
|
|
|
|
func (s testSecurityStore) UpsertIndependentPassword(types.TenantID, *types.IndependentPassword) error {
|
|
return s.err
|
|
}
|
|
|
|
func (s testSecurityStore) UpdateIndependentPasswordAttempts(types.TenantID, types.Uid, string, int, *time.Time, time.Time) error {
|
|
return s.err
|
|
}
|
|
|
|
func (s testSecurityStore) GetYouthMode(types.TenantID, types.Uid) (*types.YouthModeSettings, error) {
|
|
return nil, types.ErrNotFound
|
|
}
|
|
|
|
func (s testSecurityStore) SetYouthMode(types.TenantID, types.Uid, bool, int, time.Time) (*types.YouthModeSettings, error) {
|
|
return nil, s.err
|
|
}
|
|
|
|
func (s testSecurityStore) CreateSystemNotification(types.TenantID, *types.SystemNotification) error {
|
|
return s.err
|
|
}
|
|
|
|
func (s testSecurityStore) ListSystemNotifications(types.TenantID, types.Uid, string, int) ([]types.SystemNotification, string, error) {
|
|
return nil, "", s.err
|
|
}
|
|
|
|
func (s testSecurityStore) CountUnreadSystemNotifications(types.TenantID, types.Uid) (int, error) {
|
|
return 0, s.err
|
|
}
|
|
|
|
func (s testSecurityStore) CountUnreadSystemNotificationsByEventPrefix(types.TenantID, types.Uid, string) (int, error) {
|
|
return 0, s.err
|
|
}
|
|
|
|
func (s testSecurityStore) MarkSystemNotificationRead(_ types.TenantID, _ types.Uid, eventID string, readAt time.Time) (*types.SystemNotification, error) {
|
|
if s.err != nil {
|
|
return nil, s.err
|
|
}
|
|
return &types.SystemNotification{EventID: eventID, EventType: "system.notice", Title: "通知", Summary: "通知", ReadAt: &readAt, CreatedAt: readAt}, nil
|
|
}
|
|
|
|
func newTestAuthenticator(t *testing.T) *authenticator {
|
|
t.Helper()
|
|
conf, err := json.Marshal(map[string]any{
|
|
"key": []byte("0123456789abcdef0123456789abcdef"),
|
|
"serial_num": 3,
|
|
"expire_in": 3600,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ta := &authenticator{}
|
|
if err = ta.Init(conf, "token-test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return ta
|
|
}
|
|
|
|
func TestTokenV3TenantRoundTrip(t *testing.T) {
|
|
oldSecurity := store.Security
|
|
store.Security = testSecurityStore{version: 11}
|
|
defer func() { store.Security = oldSecurity }()
|
|
|
|
ta := newTestAuthenticator(t)
|
|
rec := &auth.Rec{
|
|
TenantID: 7,
|
|
Uid: 42,
|
|
AuthLevel: auth.LevelAuth,
|
|
Lifetime: auth.Duration(time.Hour),
|
|
Features: auth.FeatureValidated,
|
|
AuthVersion: 11,
|
|
}
|
|
token, _, err := ta.GenSecret(rec)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, want := len(token), 63; got != want {
|
|
t.Fatalf("token length: got %d, want %d", got, want)
|
|
}
|
|
|
|
got, challenge, err := ta.Authenticate(auth.AuthContext{TenantID: 7}, token)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if challenge != nil {
|
|
t.Fatal("unexpected challenge")
|
|
}
|
|
if got.TenantID != rec.TenantID || got.Uid != rec.Uid || got.AuthLevel != rec.AuthLevel ||
|
|
got.Features != rec.Features || got.AuthVersion != rec.AuthVersion {
|
|
t.Fatalf("round trip mismatch: got %+v, want %+v", got, rec)
|
|
}
|
|
|
|
// HTTP authentication may recover the tenant from the signed token.
|
|
got, _, err = ta.Authenticate(auth.AuthContext{}, token)
|
|
if err != nil || got.TenantID != rec.TenantID {
|
|
t.Fatalf("tenant recovery failed: rec=%+v err=%v", got, err)
|
|
}
|
|
}
|
|
|
|
func TestTokenV3RejectsWrongOrTamperedTenant(t *testing.T) {
|
|
oldSecurity := store.Security
|
|
store.Security = testSecurityStore{version: 1}
|
|
defer func() { store.Security = oldSecurity }()
|
|
|
|
ta := newTestAuthenticator(t)
|
|
token, _, err := ta.GenSecret(&auth.Rec{
|
|
TenantID: 7, Uid: 42, AuthLevel: auth.LevelAuth, Lifetime: auth.Duration(time.Hour), AuthVersion: 1,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, _, err = ta.Authenticate(auth.AuthContext{TenantID: 8}, token); err != types.ErrFailed {
|
|
t.Fatalf("wrong tenant: got %v, want %v", err, types.ErrFailed)
|
|
}
|
|
|
|
tampered := append([]byte(nil), token...)
|
|
tampered[1] ^= 1 // First byte of tenant_id in the little-endian payload.
|
|
if _, _, err = ta.Authenticate(auth.AuthContext{}, tampered); err != types.ErrFailed {
|
|
t.Fatalf("tampered tenant: got %v, want %v", err, types.ErrFailed)
|
|
}
|
|
}
|
|
|
|
func TestTokenV3RejectsLegacyMissingIdentityAndStaleVersion(t *testing.T) {
|
|
oldSecurity := store.Security
|
|
store.Security = testSecurityStore{version: 2}
|
|
defer func() { store.Security = oldSecurity }()
|
|
|
|
ta := newTestAuthenticator(t)
|
|
if _, _, err := ta.Authenticate(auth.AuthContext{}, make([]byte, 50)); err != types.ErrMalformed {
|
|
t.Fatalf("legacy token: got %v, want %v", err, types.ErrMalformed)
|
|
}
|
|
if _, _, err := ta.GenSecret(&auth.Rec{Uid: 42, AuthLevel: auth.LevelAuth}); err != types.ErrMalformed {
|
|
t.Fatalf("missing tenant: got %v, want %v", err, types.ErrMalformed)
|
|
}
|
|
if _, _, err := ta.GenSecret(&auth.Rec{TenantID: 7, AuthLevel: auth.LevelAuth}); err != types.ErrMalformed {
|
|
t.Fatalf("missing uid: got %v, want %v", err, types.ErrMalformed)
|
|
}
|
|
|
|
token, _, err := ta.GenSecret(&auth.Rec{
|
|
TenantID: 7, Uid: 42, AuthLevel: auth.LevelAuth, Lifetime: auth.Duration(time.Hour), AuthVersion: 1,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err = ta.Authenticate(auth.AuthContext{TenantID: 7}, token); err != types.ErrFailed {
|
|
t.Fatalf("stale auth version: got %v, want %v", err, types.ErrFailed)
|
|
}
|
|
}
|
|
|
|
func TestTokenV3NoLoginDoesNotRequireAuthVersion(t *testing.T) {
|
|
oldSecurity := store.Security
|
|
store.Security = testSecurityStore{version: 99}
|
|
defer func() { store.Security = oldSecurity }()
|
|
|
|
ta := newTestAuthenticator(t)
|
|
token, _, err := ta.GenSecret(&auth.Rec{
|
|
TenantID: 7, Uid: 42, AuthLevel: auth.LevelAuth, Lifetime: auth.Duration(time.Hour), Features: auth.FeatureNoLogin,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, _, err := ta.Authenticate(auth.AuthContext{TenantID: 7}, token)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.AuthVersion != 0 || got.Features&auth.FeatureNoLogin == 0 {
|
|
t.Fatalf("unexpected no-login token auth: %+v", got)
|
|
}
|
|
}
|