feat: add im auth verification flows
This commit is contained in:
@@ -247,6 +247,8 @@ type Rec struct {
|
||||
State types.ObjState
|
||||
// Credential 'method:value' associated with this record.
|
||||
Credential string `json:"cred,omitempty"`
|
||||
// AuthVersion invalidates previously issued login tokens when incremented.
|
||||
AuthVersion int `json:"auth_version,omitempty"`
|
||||
|
||||
// Authenticator may request the server to create a new account.
|
||||
// These are the account parameters which can be used for creating the account.
|
||||
|
||||
@@ -23,10 +23,10 @@ type authenticator struct {
|
||||
serialNumber int
|
||||
}
|
||||
|
||||
const tokenVersion = uint8(2)
|
||||
const tokenVersion = uint8(3)
|
||||
|
||||
// tokenLayout defines positioning of various bytes in token v2.
|
||||
// [1:version][8:tenant][8:UID][4:expires][2:authLevel][2:serial][2:features][32:signature] = 59 bytes.
|
||||
// tokenLayout defines positioning of various bytes in token v3.
|
||||
// [1:version][8:tenant][8:UID][4:expires][2:authLevel][2:serial][2:features][4:authVersion][32:signature] = 63 bytes.
|
||||
type tokenLayout struct {
|
||||
// Token format version.
|
||||
Version uint8
|
||||
@@ -42,6 +42,8 @@ type tokenLayout struct {
|
||||
SerialNumber uint16
|
||||
// Bitmap with feature bits.
|
||||
Features uint16
|
||||
// User-scoped auth version.
|
||||
AuthVersion uint32
|
||||
}
|
||||
|
||||
// Init initializes the authenticator: parses the config and sets salt, serial number and lifetime.
|
||||
@@ -143,13 +145,29 @@ func (ta *authenticator) Authenticate(ctx auth.AuthContext, token []byte) (*auth
|
||||
return nil, nil, types.ErrExpired
|
||||
}
|
||||
|
||||
features := auth.Feature(tl.Features)
|
||||
if features&auth.FeatureNoLogin == 0 {
|
||||
if store.Security == nil {
|
||||
return nil, nil, types.ErrInternal
|
||||
}
|
||||
currentVersion, err := store.Security.GetAuthVersion(types.TenantID(tl.TenantID), types.Uid(tl.Uid))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if int(tl.AuthVersion) != currentVersion {
|
||||
return nil, nil, types.ErrFailed
|
||||
}
|
||||
}
|
||||
|
||||
return &auth.Rec{
|
||||
TenantID: types.TenantID(tl.TenantID),
|
||||
Uid: types.Uid(tl.Uid),
|
||||
AuthLevel: auth.Level(tl.AuthLevel),
|
||||
Lifetime: auth.Duration(time.Until(expires)),
|
||||
Features: auth.Feature(tl.Features),
|
||||
State: types.StateUndefined}, nil, nil
|
||||
TenantID: types.TenantID(tl.TenantID),
|
||||
Uid: types.Uid(tl.Uid),
|
||||
AuthLevel: auth.Level(tl.AuthLevel),
|
||||
Lifetime: auth.Duration(time.Until(expires)),
|
||||
Features: features,
|
||||
State: types.StateUndefined,
|
||||
AuthVersion: int(tl.AuthVersion),
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
// GenSecret generates a new token.
|
||||
@@ -173,6 +191,7 @@ func (ta *authenticator) GenSecret(rec *auth.Rec) ([]byte, time.Time, error) {
|
||||
AuthLevel: uint16(rec.AuthLevel),
|
||||
SerialNumber: uint16(ta.serialNumber),
|
||||
Features: uint16(rec.Features),
|
||||
AuthVersion: uint32(rec.AuthVersion),
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
binary.Write(buf, binary.LittleEndian, &tl)
|
||||
|
||||
@@ -6,9 +6,23 @@ import (
|
||||
"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 newTestAuthenticator(t *testing.T) *authenticator {
|
||||
t.Helper()
|
||||
conf, err := json.Marshal(map[string]any{
|
||||
@@ -26,20 +40,25 @@ func newTestAuthenticator(t *testing.T) *authenticator {
|
||||
return ta
|
||||
}
|
||||
|
||||
func TestTokenV2TenantRoundTrip(t *testing.T) {
|
||||
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,
|
||||
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), 59; got != want {
|
||||
if got, want := len(token), 63; got != want {
|
||||
t.Fatalf("token length: got %d, want %d", got, want)
|
||||
}
|
||||
|
||||
@@ -50,7 +69,8 @@ func TestTokenV2TenantRoundTrip(t *testing.T) {
|
||||
if challenge != nil {
|
||||
t.Fatal("unexpected challenge")
|
||||
}
|
||||
if got.TenantID != rec.TenantID || got.Uid != rec.Uid || got.AuthLevel != rec.AuthLevel || got.Features != rec.Features {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -61,10 +81,14 @@ func TestTokenV2TenantRoundTrip(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenV2RejectsWrongOrTamperedTenant(t *testing.T) {
|
||||
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),
|
||||
TenantID: 7, Uid: 42, AuthLevel: auth.LevelAuth, Lifetime: auth.Duration(time.Hour), AuthVersion: 1,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -81,7 +105,11 @@ func TestTokenV2RejectsWrongOrTamperedTenant(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenV2RejectsLegacyAndMissingIdentity(t *testing.T) {
|
||||
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)
|
||||
@@ -92,4 +120,35 @@ func TestTokenV2RejectsLegacyAndMissingIdentity(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user