User password changes invalidate sessions

This commit is contained in:
advplyr 2026-07-23 17:21:25 -05:00
parent a6cde926ee
commit c900676be7
5 changed files with 112 additions and 12 deletions

View file

@ -81,7 +81,7 @@ class Auth {
* @param {import('./models/User')} user
* @param {Request} req
* @param {Response} res
* @returns {Promise<string>} accessToken only if user is current user and refresh token is valid
* @returns {Promise<{ accessToken:string, refreshToken:string }|null>} new tokens for the current session if kept alive
*/
async invalidateJwtSessionsForUser(user, req, res) {
return this.tokenManager.invalidateJwtSessionsForUser(user, req, res)

View file

@ -464,14 +464,19 @@ class TokenManager {
* @param {import('../models/User')} user
* @param {import('express').Request} req
* @param {import('express').Response} res
* @returns {Promise<string>} accessToken only if user is current user and refresh token is valid
* @returns {Promise<{ accessToken:string, refreshToken:string }|null>} new tokens for the current session if kept alive
*/
async invalidateJwtSessionsForUser(user, req, res) {
const currentRefreshToken = req.cookies.refresh_token
const currentRefreshToken = req.cookies.refresh_token || req.headers['x-refresh-token']
if (req.user.id === user.id && currentRefreshToken) {
// Current user is the same as the user to invalidate sessions for
// So rotate token for current session
const currentSession = await Database.sessionModel.findOne({ where: { refreshToken: currentRefreshToken } })
const currentSession = await Database.sessionModel.findOne({
where: {
userId: user.id,
[Op.or]: [{ refreshToken: currentRefreshToken }, { lastRefreshToken: currentRefreshToken }]
}
})
if (currentSession) {
const newTokens = await this.rotateTokensForSession(currentSession, user, req, res, false)
@ -485,7 +490,10 @@ class TokenManager {
}
})
return newTokens.accessToken
return {
accessToken: newTokens.accessToken,
refreshToken: newTokens.refreshToken
}
} else {
Logger.error(`[TokenManager] No session found to rotate tokens`)
}

View file

@ -351,6 +351,8 @@ class MeController {
* User change password. Requires current password.
* Guest users cannot change password.
*
* Invalidates all other JWT sessions for the user. If using x-refresh-token, returns new tokens for the current session.
*
* @this import('../routers/ApiRouter')
*
* @param {RequestWithUser} req
@ -373,6 +375,24 @@ class MeController {
return res.status(400).send(result.error)
}
const shouldReturnTokens = !!req.headers['x-refresh-token']
const newTokens = await this.auth.invalidateJwtSessionsForUser(req.user, req, res)
if (newTokens?.accessToken) {
Logger.info(`[MeController] Invalidated other JWT sessions for user ${req.user.username} after password change`)
if (shouldReturnTokens) {
return res.json({
success: true,
user: {
accessToken: newTokens.accessToken,
refreshToken: newTokens.refreshToken
}
})
}
} else {
Logger.info(`[MeController] Invalidated all JWT sessions for user ${req.user.username} after password change`)
}
res.sendStatus(200)
}

View file

@ -253,6 +253,7 @@ class UserController {
// Updating password
if (updatePayload.password) {
user.pash = await this.auth.localAuthStrategy.hashPassword(updatePayload.password)
shouldInvalidateJwtSessions = true
hasUpdates = true
}
@ -331,14 +332,11 @@ class UserController {
Logger.info(`[UserController] User ${user.username} has generated a new api token`)
}
// Handle JWT session invalidation for username changes
// Handle JWT session invalidation for username/password changes
if (shouldInvalidateJwtSessions) {
const newAccessToken = await this.auth.invalidateJwtSessionsForUser(user, req, res)
if (newAccessToken) {
user.accessToken = newAccessToken
// Refresh tokens are only returned for mobile clients
// Mobile apps currently do not use this API endpoint so always set to null
user.refreshToken = null
const newTokens = await this.auth.invalidateJwtSessionsForUser(user, req, res)
if (newTokens) {
// Note: for admin users changing their own password they should use MeController.updatePassword instead. This endpoint does not return tokens
Logger.info(`[UserController] Invalidated JWT sessions for user ${user.username} and rotated tokens for current session`)
} else {
Logger.info(`[UserController] Invalidated JWT sessions for user ${user.username}`)