[v10.0/forgejo] fix: delay deleting authorization token (#6976)
Some checks failed
/ release (push) Has been cancelled
testing / test-remote-cacher (redis) (push) Has been cancelled
testing / test-remote-cacher (valkey) (push) Has been cancelled
testing / test-remote-cacher (garnet) (push) Has been cancelled
testing / test-remote-cacher (redict) (push) Has been cancelled
testing / test-mysql (push) Has been cancelled
testing / backend-checks (push) Has been cancelled
testing / frontend-checks (push) Has been cancelled
testing / test-unit (push) Has been cancelled
testing / test-e2e (push) Has been cancelled
testing / test-pgsql (push) Has been cancelled
testing / test-sqlite (push) Has been cancelled
testing / security-check (push) Has been cancelled

**Backport:** https://codeberg.org/forgejo/forgejo/pulls/6937

- 1ce33aa38d extended the LTA table with a purpose column so it could be extended to other tokens. However some are single-use tokens and should be deleted after use.
- This did not result in a good UX for activating user as they needed to also fill in their passwords and in the case that the password was incorrect the token would no longer be usable.
- This patch modifies the code to allow for a little delay before deleting authorization tokens to do additional verification such as the password check. This cannot be done before the authorization token check as that the authorization token determines who the user is.
- Resolves forgejo/forgejo#6912
- Adjusted existing unit test.

Co-authored-by: Gusted <postmaster@gusted.xyz>
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6976
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: forgejo-backport-action <forgejo-backport-action@noreply.codeberg.org>
Co-committed-by: forgejo-backport-action <forgejo-backport-action@noreply.codeberg.org>
This commit is contained in:
forgejo-backport-action 2025-02-19 07:06:01 +00:00 committed by Earl Warren
parent cc8a05f693
commit 932afb2036
5 changed files with 63 additions and 26 deletions

View file

@ -842,48 +842,46 @@ func countUsers(ctx context.Context, opts *CountUserFilter) int64 {
// VerifyUserActiveCode verifies that the code is valid for the given purpose for this user. // VerifyUserActiveCode verifies that the code is valid for the given purpose for this user.
// If delete is specified, the token will be deleted. // If delete is specified, the token will be deleted.
func VerifyUserAuthorizationToken(ctx context.Context, code string, purpose auth.AuthorizationPurpose, delete bool) (*User, error) { func VerifyUserAuthorizationToken(ctx context.Context, code string, purpose auth.AuthorizationPurpose) (user *User, deleteToken func() error, err error) {
lookupKey, validator, found := strings.Cut(code, ":") lookupKey, validator, found := strings.Cut(code, ":")
if !found { if !found {
return nil, nil return nil, nil, nil
} }
authToken, err := auth.FindAuthToken(ctx, lookupKey, purpose) authToken, err := auth.FindAuthToken(ctx, lookupKey, purpose)
if err != nil { if err != nil {
if errors.Is(err, util.ErrNotExist) { if errors.Is(err, util.ErrNotExist) {
return nil, nil return nil, nil, nil
} }
return nil, err return nil, nil, err
} }
if authToken.IsExpired() { if authToken.IsExpired() {
return nil, auth.DeleteAuthToken(ctx, authToken) return nil, nil, auth.DeleteAuthToken(ctx, authToken)
} }
rawValidator, err := hex.DecodeString(validator) rawValidator, err := hex.DecodeString(validator)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
if subtle.ConstantTimeCompare([]byte(authToken.HashedValidator), []byte(auth.HashValidator(rawValidator))) == 0 { if subtle.ConstantTimeCompare([]byte(authToken.HashedValidator), []byte(auth.HashValidator(rawValidator))) == 0 {
return nil, errors.New("validator doesn't match") return nil, nil, errors.New("validator doesn't match")
} }
u, err := GetUserByID(ctx, authToken.UID) u, err := GetUserByID(ctx, authToken.UID)
if err != nil { if err != nil {
if IsErrUserNotExist(err) { if IsErrUserNotExist(err) {
return nil, nil return nil, nil, nil
} }
return nil, err return nil, nil, err
} }
if delete { deleteToken = func() error {
if err := auth.DeleteAuthToken(ctx, authToken); err != nil { return auth.DeleteAuthToken(ctx, authToken)
return nil, err
}
} }
return u, nil return u, deleteToken, nil
} }
// ValidateUser check if user is valid to insert / update into database // ValidateUser check if user is valid to insert / update into database

View file

@ -741,13 +741,13 @@ func TestVerifyUserAuthorizationToken(t *testing.T) {
assert.True(t, ok) assert.True(t, ok)
t.Run("Wrong purpose", func(t *testing.T) { t.Run("Wrong purpose", func(t *testing.T) {
u, err := user_model.VerifyUserAuthorizationToken(db.DefaultContext, code, auth.PasswordReset, false) u, _, err := user_model.VerifyUserAuthorizationToken(db.DefaultContext, code, auth.PasswordReset)
require.NoError(t, err) require.NoError(t, err)
assert.Nil(t, u) assert.Nil(t, u)
}) })
t.Run("No delete", func(t *testing.T) { t.Run("No delete", func(t *testing.T) {
u, err := user_model.VerifyUserAuthorizationToken(db.DefaultContext, code, auth.UserActivation, false) u, _, err := user_model.VerifyUserAuthorizationToken(db.DefaultContext, code, auth.UserActivation)
require.NoError(t, err) require.NoError(t, err)
assert.EqualValues(t, user.ID, u.ID) assert.EqualValues(t, user.ID, u.ID)
@ -757,9 +757,10 @@ func TestVerifyUserAuthorizationToken(t *testing.T) {
}) })
t.Run("Delete", func(t *testing.T) { t.Run("Delete", func(t *testing.T) {
u, err := user_model.VerifyUserAuthorizationToken(db.DefaultContext, code, auth.UserActivation, true) u, deleteToken, err := user_model.VerifyUserAuthorizationToken(db.DefaultContext, code, auth.UserActivation)
require.NoError(t, err) require.NoError(t, err)
assert.EqualValues(t, user.ID, u.ID) assert.EqualValues(t, user.ID, u.ID)
require.NoError(t, deleteToken())
authToken, err := auth.FindAuthToken(db.DefaultContext, lookupKey, auth.UserActivation) authToken, err := auth.FindAuthToken(db.DefaultContext, lookupKey, auth.UserActivation)
require.ErrorIs(t, err, util.ErrNotExist) require.ErrorIs(t, err, util.ErrNotExist)

View file

@ -61,7 +61,7 @@ func autoSignIn(ctx *context.Context) (bool, error) {
return false, nil return false, nil
} }
u, err := user_model.VerifyUserAuthorizationToken(ctx, authCookie, auth.LongTermAuthorization, false) u, _, err := user_model.VerifyUserAuthorizationToken(ctx, authCookie, auth.LongTermAuthorization)
if err != nil { if err != nil {
return false, fmt.Errorf("VerifyUserAuthorizationToken: %w", err) return false, fmt.Errorf("VerifyUserAuthorizationToken: %w", err)
} }
@ -677,7 +677,7 @@ func Activate(ctx *context.Context) {
return return
} }
user, err := user_model.VerifyUserAuthorizationToken(ctx, code, auth.UserActivation, false) user, deleteToken, err := user_model.VerifyUserAuthorizationToken(ctx, code, auth.UserActivation)
if err != nil { if err != nil {
ctx.ServerError("VerifyUserAuthorizationToken", err) ctx.ServerError("VerifyUserAuthorizationToken", err)
return return
@ -698,6 +698,11 @@ func Activate(ctx *context.Context) {
return return
} }
if err := deleteToken(); err != nil {
ctx.ServerError("deleteToken", err)
return
}
handleAccountActivation(ctx, user) handleAccountActivation(ctx, user)
} }
@ -746,7 +751,7 @@ func ActivatePost(ctx *context.Context) {
return return
} }
user, err := user_model.VerifyUserAuthorizationToken(ctx, code, auth.UserActivation, true) user, deleteToken, err := user_model.VerifyUserAuthorizationToken(ctx, code, auth.UserActivation)
if err != nil { if err != nil {
ctx.ServerError("VerifyUserAuthorizationToken", err) ctx.ServerError("VerifyUserAuthorizationToken", err)
return return
@ -775,6 +780,11 @@ func ActivatePost(ctx *context.Context) {
} }
} }
if err := deleteToken(); err != nil {
ctx.ServerError("deleteToken", err)
return
}
handleAccountActivation(ctx, user) handleAccountActivation(ctx, user)
} }
@ -835,7 +845,7 @@ func ActivateEmail(ctx *context.Context) {
code := ctx.FormString("code") code := ctx.FormString("code")
emailStr := ctx.FormString("email") emailStr := ctx.FormString("email")
u, err := user_model.VerifyUserAuthorizationToken(ctx, code, auth.EmailActivation(emailStr), true) u, deleteToken, err := user_model.VerifyUserAuthorizationToken(ctx, code, auth.EmailActivation(emailStr))
if err != nil { if err != nil {
ctx.ServerError("VerifyUserAuthorizationToken", err) ctx.ServerError("VerifyUserAuthorizationToken", err)
return return
@ -845,6 +855,11 @@ func ActivateEmail(ctx *context.Context) {
return return
} }
if err := deleteToken(); err != nil {
ctx.ServerError("deleteToken", err)
return
}
email, err := user_model.GetEmailAddressOfUser(ctx, emailStr, u.ID) email, err := user_model.GetEmailAddressOfUser(ctx, emailStr, u.ID)
if err != nil { if err != nil {
ctx.ServerError("GetEmailAddressOfUser", err) ctx.ServerError("GetEmailAddressOfUser", err)

View file

@ -116,7 +116,7 @@ func commonResetPassword(ctx *context.Context, shouldDeleteToken bool) (*user_mo
} }
// Fail early, don't frustrate the user // Fail early, don't frustrate the user
u, err := user_model.VerifyUserAuthorizationToken(ctx, code, auth.PasswordReset, shouldDeleteToken) u, deleteToken, err := user_model.VerifyUserAuthorizationToken(ctx, code, auth.PasswordReset)
if err != nil { if err != nil {
ctx.ServerError("VerifyUserAuthorizationToken", err) ctx.ServerError("VerifyUserAuthorizationToken", err)
return nil, nil return nil, nil
@ -127,6 +127,13 @@ func commonResetPassword(ctx *context.Context, shouldDeleteToken bool) (*user_mo
return nil, nil return nil, nil
} }
if shouldDeleteToken {
if err := deleteToken(); err != nil {
ctx.ServerError("deleteToken", err)
return nil, nil
}
}
twofa, err := auth.GetTwoFactorByUID(ctx, u.ID) twofa, err := auth.GetTwoFactorByUID(ctx, u.ID)
if err != nil { if err != nil {
if !auth.IsErrTwoFactorNotEnrolled(err) { if !auth.IsErrTwoFactorNotEnrolled(err) {

View file

@ -886,11 +886,27 @@ func TestUserActivate(t *testing.T) {
assert.False(t, authToken.IsExpired()) assert.False(t, authToken.IsExpired())
assert.EqualValues(t, authToken.HashedValidator, auth_model.HashValidator(rawValidator)) assert.EqualValues(t, authToken.HashedValidator, auth_model.HashValidator(rawValidator))
req = NewRequest(t, "POST", "/user/activate?code="+code) t.Run("No password", func(t *testing.T) {
session.MakeRequest(t, req, http.StatusOK) defer tests.PrintCurrentTest(t)()
unittest.AssertNotExistsBean(t, &auth_model.AuthorizationToken{ID: authToken.ID}) req = NewRequest(t, "POST", "/user/activate?code="+code)
unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "doesnotexist", IsActive: true}) session.MakeRequest(t, req, http.StatusOK)
unittest.AssertExistsIf(t, true, &auth_model.AuthorizationToken{ID: authToken.ID})
unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "doesnotexist"}, "is_active = false")
})
t.Run("With password", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req = NewRequestWithValues(t, "POST", "/user/activate?code="+code, map[string]string{
"password": "examplePassword!1",
})
session.MakeRequest(t, req, http.StatusSeeOther)
unittest.AssertExistsIf(t, false, &auth_model.AuthorizationToken{ID: authToken.ID})
unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "doesnotexist"}, "is_active = true")
})
} }
func TestUserPasswordReset(t *testing.T) { func TestUserPasswordReset(t *testing.T) {