[#4066] mark user as verified on confirm password reset

This commit is contained in:
Gani Georgiev 2024-01-13 17:52:34 +02:00
parent cd2fc536ca
commit af7c6d8d9b
3 changed files with 145 additions and 2 deletions

View File

@ -2,6 +2,9 @@
- Added Bitbucket OAuth2 provider ([#3948](https://github.com/pocketbase/pocketbase/pull/3948); thanks @aabajyan).
- Mark user as verified on confirm password reset ([#4066](https://github.com/pocketbase/pocketbase/issues/4066)).
_If the user email has changed after issuing the reset token (eg. updated from the Admin UI), then the `verified` user state remains unchanged._
- Added `TestMailer.SentMessages` field that holds all sent test app emails until cleanup.
- Minor Admin UI improvements (reduced the min table row height, added new TinyMCE codesample languages, etc.)

View File

@ -644,7 +644,7 @@ func TestRecordAuthConfirmPasswordReset(t *testing.T) {
},
},
{
Name: "valid token and data",
Name: "valid token and data (unverified user)",
Method: http.MethodPost,
Url: "/api/collections/users/confirm-password-reset",
Body: strings.NewReader(`{
@ -659,6 +659,132 @@ func TestRecordAuthConfirmPasswordReset(t *testing.T) {
"OnRecordBeforeConfirmPasswordResetRequest": 1,
"OnRecordAfterConfirmPasswordResetRequest": 1,
},
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
user, err := app.Dao().FindAuthRecordByEmail("users", "test@example.com")
if err != nil {
t.Fatalf("Failed to fetch confirm password user: %v", err)
}
if user.Verified() {
t.Fatalf("Expected the user to be unverified")
}
},
AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) {
user, err := app.Dao().FindAuthRecordByToken(
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZXhwIjoyMjA4OTg1MjYxfQ.R_4FOSUHIuJQ5Crl3PpIPCXMsoHzuTaNlccpXg_3FOg",
app.Settings().RecordPasswordResetToken.Secret,
)
if err == nil {
t.Fatalf("Expected the password reset token to be invalidated")
}
user, err = app.Dao().FindAuthRecordByEmail("users", "test@example.com")
if err != nil {
t.Fatalf("Failed to fetch confirm password user: %v", err)
}
if !user.Verified() {
t.Fatalf("Expected the user to be marked as verified")
}
},
},
{
Name: "valid token and data (unverified user with different email from the one in the token)",
Method: http.MethodPost,
Url: "/api/collections/users/confirm-password-reset",
Body: strings.NewReader(`{
"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZXhwIjoyMjA4OTg1MjYxfQ.R_4FOSUHIuJQ5Crl3PpIPCXMsoHzuTaNlccpXg_3FOg",
"password":"12345678",
"passwordConfirm":"12345678"
}`),
ExpectedStatus: 204,
ExpectedEvents: map[string]int{
"OnModelAfterUpdate": 1,
"OnModelBeforeUpdate": 1,
"OnRecordBeforeConfirmPasswordResetRequest": 1,
"OnRecordAfterConfirmPasswordResetRequest": 1,
},
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
user, err := app.Dao().FindAuthRecordByEmail("users", "test@example.com")
if err != nil {
t.Fatalf("Failed to fetch confirm password user: %v", err)
}
if user.Verified() {
t.Fatalf("Expected the user to be unverified")
}
// manually change the email to check whether the verified state will be updated
user.SetEmail("test_update@example.com")
if err := app.Dao().WithoutHooks().SaveRecord(user); err != nil {
t.Fatalf("Failed to update user test email")
}
},
AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) {
user, err := app.Dao().FindAuthRecordByToken(
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZXhwIjoyMjA4OTg1MjYxfQ.R_4FOSUHIuJQ5Crl3PpIPCXMsoHzuTaNlccpXg_3FOg",
app.Settings().RecordPasswordResetToken.Secret,
)
if err == nil {
t.Fatalf("Expected the password reset token to be invalidated")
}
user, err = app.Dao().FindAuthRecordByEmail("users", "test_update@example.com")
if err != nil {
t.Fatalf("Failed to fetch confirm password user: %v", err)
}
if user.Verified() {
t.Fatalf("Expected the user to remain unverified")
}
},
},
{
Name: "valid token and data (verified user)",
Method: http.MethodPost,
Url: "/api/collections/users/confirm-password-reset",
Body: strings.NewReader(`{
"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZXhwIjoyMjA4OTg1MjYxfQ.R_4FOSUHIuJQ5Crl3PpIPCXMsoHzuTaNlccpXg_3FOg",
"password":"12345678",
"passwordConfirm":"12345678"
}`),
ExpectedStatus: 204,
ExpectedEvents: map[string]int{
"OnModelAfterUpdate": 1,
"OnModelBeforeUpdate": 1,
"OnRecordBeforeConfirmPasswordResetRequest": 1,
"OnRecordAfterConfirmPasswordResetRequest": 1,
},
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
user, err := app.Dao().FindAuthRecordByEmail("users", "test@example.com")
if err != nil {
t.Fatalf("Failed to fetch confirm password user: %v", err)
}
// ensure that the user is already verified
user.SetVerified(true)
if err := app.Dao().WithoutHooks().SaveRecord(user); err != nil {
t.Fatalf("Failed to update user verified state")
}
},
AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) {
user, err := app.Dao().FindAuthRecordByToken(
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZXhwIjoyMjA4OTg1MjYxfQ.R_4FOSUHIuJQ5Crl3PpIPCXMsoHzuTaNlccpXg_3FOg",
app.Settings().RecordPasswordResetToken.Secret,
)
if err == nil {
t.Fatalf("Expected the password reset token to be invalidated")
}
user, err = app.Dao().FindAuthRecordByEmail("users", "test@example.com")
if err != nil {
t.Fatalf("Failed to fetch confirm password user: %v", err)
}
if !user.Verified() {
t.Fatalf("Expected the user to remain verified")
}
},
},
{
Name: "OnRecordAfterConfirmPasswordResetRequest error response",

View File

@ -6,6 +6,8 @@ import (
"github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/forms/validators"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/tools/security"
"github.com/spf13/cast"
)
// RecordPasswordResetConfirm is an auth record password reset confirmation form.
@ -91,9 +93,21 @@ func (form *RecordPasswordResetConfirm) Submit(interceptors ...InterceptorFunc[*
return nil, err
}
if !authRecord.Verified() {
payload, err := security.ParseUnverifiedJWT(form.Token)
if err != nil {
return nil, err
}
// mark as verified if the email hasn't changed
if authRecord.Email() == cast.ToString(payload["email"]) {
authRecord.SetVerified(true)
}
}
interceptorsErr := runInterceptors(authRecord, func(m *models.Record) error {
authRecord = m
return form.dao.SaveRecord(m)
return form.dao.SaveRecord(authRecord)
}, interceptors...)
if interceptorsErr != nil {