added more tests for internal record hooks
This commit is contained in:
@@ -65,10 +65,19 @@ func recordAuthWithOTP(e *core.RequestEvent) error {
|
|||||||
// ---
|
// ---
|
||||||
|
|
||||||
return e.App.OnRecordAuthWithOTPRequest().Trigger(event, func(e *core.RecordAuthWithOTPRequestEvent) error {
|
return e.App.OnRecordAuthWithOTPRequest().Trigger(event, func(e *core.RecordAuthWithOTPRequestEvent) error {
|
||||||
|
otpId := e.OTP.Id
|
||||||
|
otpSentTo := e.OTP.SentTo()
|
||||||
|
|
||||||
|
// eagerly delete the OTP to avoid unnecessery double delete model hook calls
|
||||||
|
// triggered by the password change below
|
||||||
|
err := e.App.Delete(e.OTP)
|
||||||
|
if err != nil {
|
||||||
|
e.App.Logger().Error("Failed to delete used OTP", "error", err, "otpId", e.OTP.Id)
|
||||||
|
}
|
||||||
|
|
||||||
// update the user email verified state in case the OTP originate from an email address matching the current record one
|
// update the user email verified state in case the OTP originate from an email address matching the current record one
|
||||||
//
|
//
|
||||||
// note: don't wait for success auth response (it could fail because of MFA) and because we already validated the OTP above
|
// note: don't wait for success auth response (it could fail because of MFA) and because we already validated the OTP above
|
||||||
otpSentTo := e.OTP.SentTo()
|
|
||||||
if !e.Record.Verified() && otpSentTo != "" && e.Record.Email() == otpSentTo {
|
if !e.Record.Verified() && otpSentTo != "" && e.Record.Email() == otpSentTo {
|
||||||
e.Record.SetVerified(true)
|
e.Record.SetVerified(true)
|
||||||
|
|
||||||
@@ -82,18 +91,12 @@ func recordAuthWithOTP(e *core.RequestEvent) error {
|
|||||||
if err := e.App.Save(e.Record); err != nil {
|
if err := e.App.Save(e.Record); err != nil {
|
||||||
e.App.Logger().Error("Failed to update record verified state after successful OTP validation",
|
e.App.Logger().Error("Failed to update record verified state after successful OTP validation",
|
||||||
"error", err,
|
"error", err,
|
||||||
"otpId", e.OTP.Id,
|
"otpId", otpId,
|
||||||
"recordId", e.Record.Id,
|
"recordId", e.Record.Id,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to delete the used otp
|
|
||||||
err = e.App.Delete(e.OTP)
|
|
||||||
if err != nil {
|
|
||||||
e.App.Logger().Error("Failed to delete used OTP", "error", err, "otpId", e.OTP.Id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return RecordAuthResponse(e.RequestEvent, e.Record, core.MFAMethodOTP, nil)
|
return RecordAuthResponse(e.RequestEvent, e.Record, core.MFAMethodOTP, nil)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -406,10 +406,10 @@ func TestRecordAuthWithOTP(t *testing.T) {
|
|||||||
"OnModelCreate": 1,
|
"OnModelCreate": 1,
|
||||||
"OnModelCreateExecute": 1,
|
"OnModelCreateExecute": 1,
|
||||||
"OnModelAfterCreateSuccess": 1,
|
"OnModelAfterCreateSuccess": 1,
|
||||||
// 2 record OTPs + 2 ExternalAuths delete
|
// record OTP + 2 ExternalAuths delete
|
||||||
"OnModelDelete": 4,
|
"OnModelDelete": 3,
|
||||||
"OnModelDeleteExecute": 4,
|
"OnModelDeleteExecute": 3,
|
||||||
"OnModelAfterDeleteSuccess": 4,
|
"OnModelAfterDeleteSuccess": 3,
|
||||||
// user verified update
|
// user verified update
|
||||||
"OnModelUpdate": 1,
|
"OnModelUpdate": 1,
|
||||||
"OnModelUpdateExecute": 1,
|
"OnModelUpdateExecute": 1,
|
||||||
@@ -419,9 +419,9 @@ func TestRecordAuthWithOTP(t *testing.T) {
|
|||||||
"OnRecordCreate": 1,
|
"OnRecordCreate": 1,
|
||||||
"OnRecordCreateExecute": 1,
|
"OnRecordCreateExecute": 1,
|
||||||
"OnRecordAfterCreateSuccess": 1,
|
"OnRecordAfterCreateSuccess": 1,
|
||||||
"OnRecordDelete": 4,
|
"OnRecordDelete": 3,
|
||||||
"OnRecordDeleteExecute": 4,
|
"OnRecordDeleteExecute": 3,
|
||||||
"OnRecordAfterDeleteSuccess": 4,
|
"OnRecordAfterDeleteSuccess": 3,
|
||||||
"OnRecordUpdate": 1,
|
"OnRecordUpdate": 1,
|
||||||
"OnRecordUpdateExecute": 1,
|
"OnRecordUpdateExecute": 1,
|
||||||
"OnRecordAfterUpdateSuccess": 1,
|
"OnRecordAfterUpdateSuccess": 1,
|
||||||
|
|||||||
@@ -308,3 +308,104 @@ func TestExternalAuthValidateHook(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExternalAuthClearOnVerfiedUpgrade(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
app, _ := tests.NewTestApp()
|
||||||
|
defer app.Cleanup()
|
||||||
|
|
||||||
|
t.Run("unverified->no changes", func(t *testing.T) {
|
||||||
|
user, err := app.FindAuthRecordByEmail("users", "test@example.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Verified() {
|
||||||
|
t.Fatal("Expected user to be unverified")
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAuths, err := app.FindAllExternalAuthsByRecord(user)
|
||||||
|
if err != nil || len(beforeAuths) == 0 {
|
||||||
|
t.Fatalf("Expected at least one external auth (%v)", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
oldTokenKey := user.TokenKey()
|
||||||
|
|
||||||
|
if err = app.Save(user); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if oldTokenKey != user.TokenKey() {
|
||||||
|
t.Fatal("Expected tokenKey to remain unchanged")
|
||||||
|
}
|
||||||
|
|
||||||
|
afterAuths, err := app.FindAllExternalAuthsByRecord(user)
|
||||||
|
if err != nil || len(afterAuths) != len(beforeAuths) {
|
||||||
|
t.Fatalf("Expected %d external auths, found %d (%v)", len(afterAuths), len(beforeAuths), err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("unverified->verified", func(t *testing.T) {
|
||||||
|
user, err := app.FindAuthRecordByEmail("users", "test@example.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Verified() {
|
||||||
|
t.Fatal("Expected user to be unverified")
|
||||||
|
}
|
||||||
|
|
||||||
|
externalAuths, err := app.FindAllExternalAuthsByRecord(user)
|
||||||
|
if err != nil || len(externalAuths) == 0 {
|
||||||
|
t.Fatalf("Expected at least one external auth (%v)", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
oldTokenKey := user.TokenKey()
|
||||||
|
|
||||||
|
user.SetVerified(true)
|
||||||
|
if err = app.Save(user); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if oldTokenKey == user.TokenKey() {
|
||||||
|
t.Fatal("Expected tokenKey to be renewed")
|
||||||
|
}
|
||||||
|
|
||||||
|
externalAuths, err = app.FindAllExternalAuthsByRecord(user)
|
||||||
|
if err != nil || len(externalAuths) != 0 {
|
||||||
|
t.Fatalf("Expected all user external auths to be deleted, found %d (%v)", len(externalAuths), err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("verified->no changes", func(t *testing.T) {
|
||||||
|
user, err := app.FindAuthRecordByEmail("users", "test3@example.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !user.Verified() {
|
||||||
|
t.Fatal("Expected user to be verified")
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAuths, err := app.FindAllExternalAuthsByRecord(user)
|
||||||
|
if err != nil || len(beforeAuths) == 0 {
|
||||||
|
t.Fatalf("Expected at least one external auth (%v)", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
oldTokenKey := user.TokenKey()
|
||||||
|
|
||||||
|
if err = app.Save(user); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if oldTokenKey != user.TokenKey() {
|
||||||
|
t.Fatal("Expected tokenKey to remain unchanged")
|
||||||
|
}
|
||||||
|
|
||||||
|
afterAuths, err := app.FindAllExternalAuthsByRecord(user)
|
||||||
|
if err != nil || len(afterAuths) != len(beforeAuths) {
|
||||||
|
t.Fatalf("Expected %d external auths, found %d (%v)", len(afterAuths), len(beforeAuths), err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -300,3 +300,64 @@ func TestMFAValidateHook(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMFAClearOnPasswordChange(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
app, _ := tests.NewTestApp()
|
||||||
|
defer app.Cleanup()
|
||||||
|
|
||||||
|
user1, err := app.FindAuthRecordByEmail("users", "test@example.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user2, err := app.FindAuthRecordByEmail("users", "test2@example.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mfasToCreate := map[*core.Record]int{
|
||||||
|
user1: 3,
|
||||||
|
user2: 2,
|
||||||
|
}
|
||||||
|
for user, total := range mfasToCreate {
|
||||||
|
for range total {
|
||||||
|
mfa := core.NewMFA(app)
|
||||||
|
mfa.SetCollectionRef(user.Collection().Id)
|
||||||
|
mfa.SetRecordRef(user.Id)
|
||||||
|
mfa.SetMethod(core.MFAMethodPassword)
|
||||||
|
if err := app.Save(mfa); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update both users
|
||||||
|
err = app.Save(user1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user2.SetRandomPassword()
|
||||||
|
err = app.Save(user2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedMFAs := map[*core.Record]int{
|
||||||
|
user1: 3,
|
||||||
|
user2: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
for user, expected := range expectedMFAs {
|
||||||
|
mfas, err := app.FindAllMFAsByRecord(user)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(mfas) != expected {
|
||||||
|
t.Fatalf("Expected %d MFAs, got %d", expected, len(mfas))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ func (app *BaseApp) registerOTPHooks() {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.Record.Original().TokenKey() != e.Record.TokenKey() {
|
if !e.Record.Original().IsNew() && e.Record.Original().TokenKey() != e.Record.TokenKey() {
|
||||||
err := e.App.DeleteAllOTPsByRecord(e.Record)
|
err := e.App.DeleteAllOTPsByRecord(e.Record)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
|
|||||||
@@ -300,3 +300,64 @@ func TestOTPValidateHook(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOTPClearOnTokenKeyChange(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
app, _ := tests.NewTestApp()
|
||||||
|
defer app.Cleanup()
|
||||||
|
|
||||||
|
user1, err := app.FindAuthRecordByEmail("users", "test@example.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user2, err := app.FindAuthRecordByEmail("users", "test2@example.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
otpsToCreate := map[*core.Record]int{
|
||||||
|
user1: 3,
|
||||||
|
user2: 2,
|
||||||
|
}
|
||||||
|
for user, total := range otpsToCreate {
|
||||||
|
for range total {
|
||||||
|
otp := core.NewOTP(app)
|
||||||
|
otp.SetCollectionRef(user.Collection().Id)
|
||||||
|
otp.SetRecordRef(user.Id)
|
||||||
|
otp.SetPassword("123456")
|
||||||
|
if err := app.Save(otp); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update both users
|
||||||
|
err = app.Save(user1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user2.RefreshTokenKey()
|
||||||
|
err = app.Save(user2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedOTPs := map[*core.Record]int{
|
||||||
|
user1: 3,
|
||||||
|
user2: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
for user, expected := range expectedOTPs {
|
||||||
|
otps, err := app.FindAllOTPsByRecord(user)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(otps) != expected {
|
||||||
|
t.Fatalf("Expected %d OTPs, got %d", expected, len(otps))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ migrate((app) => {
|
|||||||
"listRule": "@request.auth.id != '' && 1 > 0 || 'backtick` + "`" + `test' = 0",
|
"listRule": "@request.auth.id != '' && 1 > 0 || 'backtick` + "`" + `test' = 0",
|
||||||
"manageRule": "1 != 2",
|
"manageRule": "1 != 2",
|
||||||
"mfa": {
|
"mfa": {
|
||||||
"duration": 1800,
|
"duration": 600,
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"rule": ""
|
"rule": ""
|
||||||
},
|
},
|
||||||
@@ -319,7 +319,7 @@ func init() {
|
|||||||
"listRule": "@request.auth.id != '' && 1 > 0 || 'backtick` + "` + \"`\" + `" + `test' = 0",
|
"listRule": "@request.auth.id != '' && 1 > 0 || 'backtick` + "` + \"`\" + `" + `test' = 0",
|
||||||
"manageRule": "1 != 2",
|
"manageRule": "1 != 2",
|
||||||
"mfa": {
|
"mfa": {
|
||||||
"duration": 1800,
|
"duration": 600,
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"rule": ""
|
"rule": ""
|
||||||
},
|
},
|
||||||
@@ -590,7 +590,7 @@ migrate((app) => {
|
|||||||
"listRule": "@request.auth.id != '' && 1 > 0 || 'backtick` + "`" + `test' = 0",
|
"listRule": "@request.auth.id != '' && 1 > 0 || 'backtick` + "`" + `test' = 0",
|
||||||
"manageRule": "1 != 2",
|
"manageRule": "1 != 2",
|
||||||
"mfa": {
|
"mfa": {
|
||||||
"duration": 1800,
|
"duration": 600,
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"rule": ""
|
"rule": ""
|
||||||
},
|
},
|
||||||
@@ -775,7 +775,7 @@ func init() {
|
|||||||
"listRule": "@request.auth.id != '' && 1 > 0 || 'backtick` + "` + \"`\" + `" + `test' = 0",
|
"listRule": "@request.auth.id != '' && 1 > 0 || 'backtick` + "` + \"`\" + `" + `test' = 0",
|
||||||
"manageRule": "1 != 2",
|
"manageRule": "1 != 2",
|
||||||
"mfa": {
|
"mfa": {
|
||||||
"duration": 1800,
|
"duration": 600,
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"rule": ""
|
"rule": ""
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user