Skip to content

Commit db82035

Browse files
committed
Wipe user sessions from DB on password reset/change.
1 parent 347f597 commit db82035

File tree

6 files changed

+53
-10
lines changed

6 files changed

+53
-10
lines changed

cmd/auth.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,11 @@ func (a *App) doResetPassword(c echo.Context, token, email string) error {
687687
return echo.NewHTTPError(http.StatusInternalServerError, a.i18n.T("globals.messages.internalError"))
688688
}
689689

690+
// Invalidate all existing sessions for the user after password reset.
691+
if err := a.core.DeleteUserSessions(user.ID, ""); err != nil {
692+
a.log.Printf("error destroying sessions after password reset for user_id=%d: %v", user.ID, err)
693+
}
694+
690695
// Log the user in directly without forcing a manual login right after password change.
691696
if err := a.auth.SaveSession(user, "", c); err != nil {
692697
return err

cmd/users.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,13 @@ func (a *App) UpdateUser(c echo.Context) error {
173173
// Blank out the password hash in the response.
174174
user.Password = null.String{}
175175

176+
// If password was changed by admin, destroy all sessions for the given user.
177+
if u.Password.String != "" {
178+
if err := a.core.DeleteUserSessions(id, ""); err != nil {
179+
a.log.Printf("error destroying sessions on admin password change for user_id=%d: %v", id, err)
180+
}
181+
}
182+
176183
// Cache the API token for in-memory, off-DB /api/* request auth.
177184
if _, err := cacheUsers(a.core, a.auth); err != nil {
178185
return err
@@ -263,6 +270,13 @@ func (a *App) UpdateUserProfile(c echo.Context) error {
263270
return err
264271
}
265272

273+
// If password was changed, destroy all existing sessions for the user except for the current one.
274+
if u.Password.String != "" {
275+
if err := a.core.DeleteUserSessions(user.ID, auth.GetSessionID(c)); err != nil {
276+
a.log.Printf("error destroying sessions after profile password change for user_id=%d: %v", user.ID, err)
277+
}
278+
}
279+
266280
// Blank out the password hash in the response.
267281
out.Password = null.String{}
268282

internal/auth/auth.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,16 @@ func (o *Auth) SaveSession(u User, oidcToken string, c echo.Context) error {
382382
return nil
383383
}
384384

385+
// GetSessionID returns the current session ID from the echo context.
386+
func GetSessionID(c echo.Context) string {
387+
sess, ok := c.Get(SessionKey).(*simplesessions.Session)
388+
if !ok || sess == nil {
389+
return ""
390+
}
391+
392+
return sess.ID()
393+
}
394+
385395
// validateSession checks if the cookie session is valid (in the DB) and returns the session and user details.
386396
func (o *Auth) validateSession(c echo.Context) (*simplesessions.Session, User, error) {
387397
// Cookie session.

internal/core/users.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"database/sql"
55
"encoding/json"
66
"net/http"
7+
"strconv"
78

89
"github.com/knadh/listmonk/internal/auth"
910
"github.com/knadh/listmonk/internal/utils"
@@ -132,6 +133,15 @@ func (c *Core) SetTwoFA(id int, twofaType, twofaKey string) error {
132133
return nil
133134
}
134135

136+
// DeleteUserSessions deletes all sessions for a given user ID, optionally
137+
// excluding a specific session ID (to keep the current session alive).
138+
func (c *Core) DeleteUserSessions(userID int, excludeID string) error {
139+
if _, err := c.q.DeleteUserSessions.Exec(strconv.Itoa(userID), excludeID); err != nil {
140+
return err
141+
}
142+
return nil
143+
}
144+
135145
// DeleteUsers deletes a given user.
136146
func (c *Core) DeleteUsers(ids []int) error {
137147
res, err := c.q.DeleteUsers.Exec(pq.Array(ids))

models/queries.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -117,16 +117,17 @@ type Queries struct {
117117
DeleteBouncesBySubscriber *sqlx.Stmt `query:"delete-bounces-by-subscriber"`
118118
GetDBInfo string `query:"get-db-info"`
119119

120-
CreateUser *sqlx.Stmt `query:"create-user"`
121-
UpdateUser *sqlx.Stmt `query:"update-user"`
122-
UpdateUserProfile *sqlx.Stmt `query:"update-user-profile"`
123-
UpdateUserLogin *sqlx.Stmt `query:"update-user-login"`
124-
SetUserTwoFA *sqlx.Stmt `query:"set-user-twofa"`
125-
DeleteUsers *sqlx.Stmt `query:"delete-users"`
126-
GetUsers *sqlx.Stmt `query:"get-users"`
127-
GetUser *sqlx.Stmt `query:"get-user"`
128-
GetAPITokens *sqlx.Stmt `query:"get-api-tokens"`
129-
LoginUser *sqlx.Stmt `query:"login-user"`
120+
CreateUser *sqlx.Stmt `query:"create-user"`
121+
UpdateUser *sqlx.Stmt `query:"update-user"`
122+
UpdateUserProfile *sqlx.Stmt `query:"update-user-profile"`
123+
UpdateUserLogin *sqlx.Stmt `query:"update-user-login"`
124+
SetUserTwoFA *sqlx.Stmt `query:"set-user-twofa"`
125+
DeleteUsers *sqlx.Stmt `query:"delete-users"`
126+
GetUsers *sqlx.Stmt `query:"get-users"`
127+
GetUser *sqlx.Stmt `query:"get-user"`
128+
GetAPITokens *sqlx.Stmt `query:"get-api-tokens"`
129+
LoginUser *sqlx.Stmt `query:"login-user"`
130+
DeleteUserSessions *sqlx.Stmt `query:"delete-user-sessions"`
130131

131132
CreateRole *sqlx.Stmt `query:"create-role"`
132133
GetUserRoles *sqlx.Stmt `query:"get-user-roles"`

queries/users.sql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,6 @@ UPDATE users SET loggedin_at=NOW(), avatar=(CASE WHEN $2 != '' THEN $2 ELSE avat
150150

151151
-- name: set-user-twofa
152152
UPDATE users SET twofa_type=$2::twofa_type, twofa_key=$3, updated_at=NOW() WHERE id=$1;
153+
154+
-- name: delete-user-sessions
155+
DELETE FROM sessions WHERE data->>'user_id' = $1 AND ($2 = '' OR id != $2);

0 commit comments

Comments
 (0)