1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2025-03-20 22:36:00 +02:00

[#5964] refresh the token key on email change

This commit is contained in:
Gani Georgiev 2024-12-17 11:32:28 +02:00
parent 0d720c3c9d
commit 76b9051011
7 changed files with 78 additions and 38 deletions

View File

@ -10,6 +10,8 @@
- Added `mailer.Message.InlineAttachments` field for attaching inline files to an email (_aka. `cid` links_). - Added `mailer.Message.InlineAttachments` field for attaching inline files to an email (_aka. `cid` links_).
- Invalidate all record tokens when the auth record email is changed programmatically or by a superuser ([#5964](https://github.com/pocketbase/pocketbase/issues/5964)).
- ⚠️ Removed the "dry submit" when executing the collections Create API rule - ⚠️ Removed the "dry submit" when executing the collections Create API rule
(you can find more details why this change was introduced and how it could affect your app in https://github.com/pocketbase/pocketbase/discussions/6073). (you can find more details why this change was introduced and how it could affect your app in https://github.com/pocketbase/pocketbase/discussions/6073).
For most users it should be non-breaking change, BUT if you have Create API rules that uses self-references or view counters you may have to adjust them manually. For most users it should be non-breaking change, BUT if you have Create API rules that uses self-references or view counters you may have to adjust them manually.

View File

@ -38,9 +38,8 @@ func recordConfirmEmailChange(e *core.RequestEvent) error {
event.NewEmail = newEmail event.NewEmail = newEmail
return e.App.OnRecordConfirmEmailChangeRequest().Trigger(event, func(e *core.RecordConfirmEmailChangeRequestEvent) error { return e.App.OnRecordConfirmEmailChangeRequest().Trigger(event, func(e *core.RecordConfirmEmailChangeRequestEvent) error {
authRecord.Set(core.FieldNameEmail, e.NewEmail) e.Record.SetEmail(e.NewEmail)
authRecord.Set(core.FieldNameVerified, true) e.Record.SetVerified(true)
authRecord.RefreshTokenKey() // invalidate old tokens
if err := e.App.Save(e.Record); err != nil { if err := e.App.Save(e.Record); err != nil {
return firstApiError(err, e.BadRequestError("Failed to confirm email change.", err)) return firstApiError(err, e.BadRequestError("Failed to confirm email change.", err))

View File

@ -47,12 +47,12 @@ func recordConfirmPasswordReset(e *core.RequestEvent) error {
} }
} }
err = form.app.Save(authRecord) err = e.App.Save(authRecord)
if err != nil { if err != nil {
return firstApiError(err, e.BadRequestError("Failed to set new password.", err)) return firstApiError(err, e.BadRequestError("Failed to set new password.", err))
} }
form.app.Store().Remove(getPasswordResetResendKey(authRecord)) e.App.Store().Remove(getPasswordResetResendKey(authRecord))
return e.NoContent(http.StatusNoContent) return e.NoContent(http.StatusNoContent)
}) })

View File

@ -186,11 +186,20 @@ func TestRecordConfirmPasswordReset(t *testing.T) {
t.Fatal("Expected the user to be unverified") t.Fatal("Expected the user to be unverified")
} }
oldTokenKey := user.TokenKey()
// manually change the email to check whether the verified state will be updated // manually change the email to check whether the verified state will be updated
user.SetEmail("test_update@example.com") user.SetEmail("test_update@example.com")
if err := app.Save(user); err != nil { if err = app.Save(user); err != nil {
t.Fatalf("Failed to update user test email: %v", err) t.Fatalf("Failed to update user test email: %v", err)
} }
// resave with the old token key since the email change above
// would change it and will make the password token invalid
user.SetTokenKey(oldTokenKey)
if err = app.Save(user); err != nil {
t.Fatalf("Failed to restore original user tokenKey: %v", err)
}
}, },
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
_, err := app.FindAuthRecordByToken( _, err := app.FindAuthRecordByToken(

View File

@ -558,12 +558,21 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
t.Fatalf("Expected password %q to be valid", "1234567890") t.Fatalf("Expected password %q to be valid", "1234567890")
} }
oldTokenKey := user.TokenKey()
// manually unset the user email // manually unset the user email
user.SetEmail("") user.SetEmail("")
if err := app.Save(user); err != nil { if err = app.Save(user); err != nil {
t.Fatal(err) t.Fatal(err)
} }
// resave with the old token key since the email change above
// would change it and will make the password token invalid
user.SetTokenKey(oldTokenKey)
if err = app.Save(user); err != nil {
t.Fatalf("Failed to restore original user tokenKey: %v", err)
}
// register the test provider // register the test provider
auth.Providers["test"] = func() auth.Provider { auth.Providers["test"] = func() auth.Provider {
return &oauth2MockProvider{ return &oauth2MockProvider{

View File

@ -1413,13 +1413,19 @@ func onRecordValidate(e *RecordEvent) error {
func onRecordSaveExecute(e *RecordEvent) error { func onRecordSaveExecute(e *RecordEvent) error {
if e.Record.Collection().IsAuth() { if e.Record.Collection().IsAuth() {
// ensure that the token key is different on password change // ensure that the token key is regenerated on password change or email change
old := e.Record.Original() if !e.Record.IsNew() {
if !e.Record.IsNew() && lastSavedRecord, err := e.App.FindRecordById(e.Record.Collection(), e.Record.Id)
old.TokenKey() == e.Record.TokenKey() && if err != nil {
old.Get(FieldNamePassword) != e.Record.Get(FieldNamePassword) { return err
}
if lastSavedRecord.TokenKey() == e.Record.TokenKey() &&
(lastSavedRecord.Get(FieldNamePassword) != e.Record.Get(FieldNamePassword) ||
lastSavedRecord.Email() != e.Record.Email()) {
e.Record.RefreshTokenKey() e.Record.RefreshTokenKey()
} }
}
// cross-check that the auth record id is unique across all auth collections. // cross-check that the auth record id is unique across all auth collections.
authCollections, err := e.App.FindAllCollections(CollectionTypeAuth) authCollections, err := e.App.FindAllCollections(CollectionTypeAuth)

View File

@ -1822,44 +1822,59 @@ func TestRecordSaveIdUpdateNoValidation(t *testing.T) {
} }
} }
func TestRecordSaveWithChangedPassword(t *testing.T) { func TestRecordSaveWithAutoTokenKeyRefresh(t *testing.T) {
t.Parallel() t.Parallel()
app, _ := tests.NewTestApp() app, _ := tests.NewTestApp()
defer app.Cleanup() defer app.Cleanup()
record, err := app.FindAuthRecordByEmail("nologin", "test@example.com") scenarios := []struct {
name string
payload map[string]any
expectedChange bool
}{
{
"no email or password change",
map[string]any{"name": "example"},
false,
},
{
"password change",
map[string]any{"password": "1234567890"},
true,
},
{
"email change",
map[string]any{"email": "test_update@example.com"},
true,
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
record, err := app.FindFirstRecordByFilter("nologin", "1=1")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
originalTokenKey := record.TokenKey() originalTokenKey := record.TokenKey()
t.Run("no password change shouldn't change the tokenKey", func(t *testing.T) { record.Load(s.payload)
record.Set("name", "example")
if err := app.Save(record); err != nil { err = app.Save(record)
if err != nil {
t.Fatal(err) t.Fatal(err)
} }
tokenKey := record.TokenKey() newTokenKey := record.TokenKey()
if tokenKey == "" || originalTokenKey != tokenKey {
t.Fatalf("Expected tokenKey to not change, got %q VS %q", originalTokenKey, tokenKey) hasChange := originalTokenKey != newTokenKey
if hasChange != s.expectedChange {
t.Fatalf("Expected hasChange %v, got %v", s.expectedChange, hasChange)
} }
}) })
t.Run("password change should change the tokenKey", func(t *testing.T) {
record.Set("password", "1234567890")
if err := app.Save(record); err != nil {
t.Fatal(err)
} }
tokenKey := record.TokenKey()
if tokenKey == "" || originalTokenKey == tokenKey {
t.Fatalf("Expected tokenKey to change, got %q VS %q", originalTokenKey, tokenKey)
}
})
} }
func TestRecordDelete(t *testing.T) { func TestRecordDelete(t *testing.T) {