diff --git a/client/pages/account.vue b/client/pages/account.vue
index e9b5da3cb..456f001cc 100644
--- a/client/pages/account.vue
+++ b/client/pages/account.vue
@@ -69,8 +69,9 @@
-
-
logout{{ $strings.ButtonLogout }}
+
+ devices{{ $strings.ButtonLogoutAllDevices }}
+ logout{{ $strings.ButtonLogout }}
@@ -87,6 +88,7 @@ export default {
newPassword: null,
confirmPassword: null,
changingPassword: false,
+ loggingOut: false,
selectedLanguage: '',
newEReaderDevice: {
name: '',
@@ -135,7 +137,7 @@ export default {
updateLocalLanguage(lang) {
this.$setLanguageCode(lang)
},
- logout() {
+ logout(allDevices = false) {
// Disconnect from socket
if (this.$root.socket) {
console.log('Disconnecting from socket', this.$root.socket.id)
@@ -149,8 +151,10 @@ export default {
this.$store.commit('libraries/setUserPlaylists', [])
this.$store.commit('libraries/setCollections', [])
+ this.loggingOut = true
+ const url = allDevices ? '/logout?allDevices=1' : '/logout'
this.$axios
- .$post('/logout')
+ .$post(url)
.then((logoutPayload) => {
const redirect_url = logoutPayload.redirect_url
@@ -163,6 +167,9 @@ export default {
.catch((error) => {
console.error(error)
})
+ .finally(() => {
+ this.loggingOut = false
+ })
},
resetForm() {
this.password = null
diff --git a/client/strings/en-us.json b/client/strings/en-us.json
index fb2bcb281..7d9fca6b8 100644
--- a/client/strings/en-us.json
+++ b/client/strings/en-us.json
@@ -46,6 +46,7 @@
"ButtonLatest": "Latest",
"ButtonLibrary": "Library",
"ButtonLogout": "Logout",
+ "ButtonLogoutAllDevices": "Logout All Devices",
"ButtonLookup": "Lookup",
"ButtonManageTracks": "Manage Tracks",
"ButtonMapChapterTitles": "Map Chapter Titles",
diff --git a/server/Auth.js b/server/Auth.js
index 65532897d..17bd1160f 100644
--- a/server/Auth.js
+++ b/server/Auth.js
@@ -471,18 +471,23 @@ class Auth {
res.json(openIdIssuerConfig)
})
- // Logout route
+ /**
+ * Logout route
+ * Use ?allDevices=1 to destroy every session for this user instead of just the current one
+ */
router.post('/logout', async (req, res) => {
// Refresh token be alternatively be sent in the header
const refreshToken = req.cookies.refresh_token || req.headers['x-refresh-token']
+ const allDevices = req.query.allDevices === '1'
// Clear refresh token cookie
res.clearCookie('refresh_token', {
path: '/'
})
- // Invalidate the session in database using refresh token
- if (refreshToken) {
+ if (allDevices) {
+ await this.tokenManager.invalidateAllSessionsForRefreshToken(refreshToken)
+ } else if (refreshToken) {
await this.tokenManager.invalidateRefreshToken(refreshToken)
} else {
Logger.info(`[Auth] logout: No refresh token on request`)
diff --git a/server/auth/TokenManager.js b/server/auth/TokenManager.js
index 5ebc6530b..1f0417622 100644
--- a/server/auth/TokenManager.js
+++ b/server/auth/TokenManager.js
@@ -505,6 +505,25 @@ class TokenManager {
return null
}
+ /**
+ * Destroy all JWT sessions for the user that owns this refresh token
+ *
+ * @param {string} refreshToken
+ */
+ async invalidateAllSessionsForRefreshToken(refreshToken) {
+ if (!refreshToken) return
+
+ const session = await Database.sessionModel.findOne({
+ where: {
+ [Op.or]: [{ refreshToken: refreshToken }, { lastRefreshToken: refreshToken }]
+ }
+ })
+ if (!session) return
+
+ const numDeleted = await Database.sessionModel.destroy({ where: { userId: session.userId } })
+ Logger.info(`[TokenManager] Invalidated all JWT sessions for user ${session.userId}, ${numDeleted} deleted`)
+ }
+
/**
* Invalidate a refresh token - used for logout
*