fix(user): ensure deletion tokens can only be used by the user who created them

This commit is contained in:
kolaente
2025-07-23 11:18:37 +02:00
parent 2b497e6265
commit 4faf50a91f
4 changed files with 130 additions and 2 deletions

View File

@@ -16,3 +16,9 @@
token: 'tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Aei'
kind: 2
created: 2021-07-12 00:00:13
-
id: 4
user_id: 1
token: 'deletiontesttoken'
kind: 3
created: 2021-07-12 00:00:14

View File

@@ -106,8 +106,16 @@ func ConfirmDeletion(s *xorm.Session, user *User, token string) (err error) {
}
if tk == nil {
// TODO: return invalid token error
return
return ErrInvalidDeletionToken{
Token: token,
}
}
if tk.UserID != user.ID {
return ErrTokenUserMismatch{
TokenUserID: tk.UserID,
UserID: user.ID,
}
}
err = removeTokens(s, user, TokenAccountDeletion)

View File

@@ -659,3 +659,58 @@ func (err ErrInvalidUserContext) HTTPError() web.HTTPError {
Message: "Invalid user context. Please make sure the passed token is valid and try again.",
}
}
// ErrInvalidDeletionToken represents an error where the deletion token is invalid
type ErrInvalidDeletionToken struct {
Token string
}
// IsErrInvalidDeletionToken checks if an error is a ErrInvalidDeletionToken.
func IsErrInvalidDeletionToken(err error) bool {
_, ok := err.(ErrInvalidDeletionToken)
return ok
}
func (err ErrInvalidDeletionToken) Error() string {
return fmt.Sprintf("Invalid deletion token [Token: %s]", err.Token)
}
// ErrorCodeInvalidDeletionToken holds the unique world-error code of this error
const ErrorCodeInvalidDeletionToken = 1028
// HTTPError holds the http error description
func (err ErrInvalidDeletionToken) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusBadRequest,
Code: ErrorCodeInvalidDeletionToken,
Message: "Invalid deletion token provided.",
}
}
// ErrTokenUserMismatch represents an error where the token doesn't belong to the user
type ErrTokenUserMismatch struct {
TokenUserID int64
UserID int64
}
// IsErrTokenUserMismatch checks if an error is a ErrTokenUserMismatch.
func IsErrTokenUserMismatch(err error) bool {
_, ok := err.(ErrTokenUserMismatch)
return ok
}
func (err ErrTokenUserMismatch) Error() string {
return fmt.Sprintf("Token user mismatch [Token User ID: %d, User ID: %d]", err.TokenUserID, err.UserID)
}
// ErrorCodeTokenUserMismatch holds the unique world-error code of this error
const ErrorCodeTokenUserMismatch = 1029
// HTTPError holds the http error description
func (err ErrTokenUserMismatch) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusForbidden,
Code: ErrorCodeTokenUserMismatch,
Message: "This deletion token does not belong to your account.",
}
}

View File

@@ -597,3 +597,62 @@ func TestUserPasswordReset(t *testing.T) {
assert.True(t, IsErrInvalidPasswordResetToken(err))
})
}
func TestConfirmDeletion(t *testing.T) {
t.Run("normal", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
user := &User{ID: 1}
err := ConfirmDeletion(s, user, "deletiontesttoken")
require.NoError(t, err)
updatedUser, err := GetUserByID(s, 1)
require.NoError(t, err)
assert.False(t, updatedUser.DeletionScheduledAt.IsZero())
})
t.Run("invalid token", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
user := &User{ID: 1}
err := ConfirmDeletion(s, user, "invalidtoken")
require.Error(t, err)
assert.True(t, IsErrInvalidDeletionToken(err))
invalidErr := err.(ErrInvalidDeletionToken)
assert.Equal(t, "invalidtoken", invalidErr.Token)
})
t.Run("token user mismatch", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
user := &User{ID: 3}
err := ConfirmDeletion(s, user, "deletiontesttoken")
require.Error(t, err)
assert.True(t, IsErrTokenUserMismatch(err))
mismatchErr := err.(ErrTokenUserMismatch)
assert.Equal(t, int64(1), mismatchErr.TokenUserID)
assert.Equal(t, int64(3), mismatchErr.UserID)
})
t.Run("removes token after successful confirmation", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
token := "deletiontesttoken"
user := &User{ID: 1}
err := ConfirmDeletion(s, user, token)
require.NoError(t, err)
db.AssertMissing(t, "user_tokens", map[string]interface{}{
"token": token,
"kind": TokenAccountDeletion,
})
})
}