From 9e7b84f28915876564544dfa0132e8f1104f09f2 Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 18 Jul 2022 17:19:16 -0500 Subject: [PATCH] Update:JWT signing --- client/components/modals/AccountModal.vue | 11 +++++++-- client/pages/config/users/_id/index.vue | 26 ++++++++++++--------- client/store/user.js | 4 ++++ index.js | 1 - server/Auth.js | 28 +++++++++++++++++++---- server/Server.js | 9 +++++--- server/controllers/MiscController.js | 2 +- server/controllers/UserController.js | 8 ++++++- server/objects/settings/ServerSettings.js | 11 ++++++++- 9 files changed, 76 insertions(+), 24 deletions(-) diff --git a/client/components/modals/AccountModal.vue b/client/components/modals/AccountModal.vue index 8f572b22..45437458 100644 --- a/client/components/modals/AccountModal.vue +++ b/client/components/modals/AccountModal.vue @@ -146,7 +146,6 @@ export default { watch: { show: { handler(newVal) { - console.log('accoutn modal show change', newVal) if (newVal) { this.init() } @@ -162,6 +161,9 @@ export default { this.$emit('input', val) } }, + user() { + return this.$store.state.user.user + }, title() { return this.isNew ? 'Add New Account' : `Update ${(this.account || {}).username}` }, @@ -250,6 +252,12 @@ export default { this.$toast.error(`Failed to update account: ${data.error}`) } else { console.log('Account updated', data.user) + + if (data.user.id === this.user.id && data.user.token !== this.user.token) { + console.log('Current user token was updated') + this.$store.commit('user/setUserToken', data.user.token) + } + this.$toast.success('Account updated') this.show = false } @@ -305,7 +313,6 @@ export default { this.isNew = !this.account if (this.account) { - console.log(this.account) this.newUser = { username: this.account.username, password: this.account.password, diff --git a/client/pages/config/users/_id/index.vue b/client/pages/config/users/_id/index.vue index ed234e35..78b9d0e0 100644 --- a/client/pages/config/users/_id/index.vue +++ b/client/pages/config/users/_id/index.vue @@ -13,11 +13,12 @@

{{ username }}

-
-

- API Token:
{{ userToken }}content_copy -

+
+ + +
+ content_copy +
@@ -138,12 +139,15 @@ export default { this.$copyToClipboard(str, this) }, async init() { - this.listeningSessions = await this.$axios.$get(`/api/users/${this.user.id}/listening-sessions?page=0&itemsPerPage=10`).then((data) => { - return data.sessions || [] - }).catch((err) => { - console.error('Failed to load listening sesions', err) - return [] - }) + this.listeningSessions = await this.$axios + .$get(`/api/users/${this.user.id}/listening-sessions?page=0&itemsPerPage=10`) + .then((data) => { + return data.sessions || [] + }) + .catch((err) => { + console.error('Failed to load listening sesions', err) + return [] + }) this.listeningStats = await this.$axios.$get(`/api/users/${this.user.id}/listening-stats`).catch((err) => { console.error('Failed to load listening sesions', err) return [] diff --git a/client/store/user.js b/client/store/user.js index 9913c684..c5daa9ce 100644 --- a/client/store/user.js +++ b/client/store/user.js @@ -136,6 +136,10 @@ export const mutations = { localStorage.removeItem('token') } }, + setUserToken(state, token) { + state.user.token = token + localStorage.setItem('token', user.token) + }, updateMediaProgress(state, { id, data }) { if (!state.user) return if (!data) { diff --git a/index.js b/index.js index 4400312e..c15a9e0b 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,3 @@ -if (process.env.TOKEN_SECRET == null) process.env.TOKEN_SECRET = '09f26e402586e2faa8da4c98a35f1b20d6b033c6097befa8be3486a829587fe2f90a832bd3ff9d42710a4da095a2ce285b009f0c3730cd9b8e1af3eb84df6611' const server = require('./server/Server') global.appRoot = __dirname diff --git a/server/Auth.js b/server/Auth.js index 29fdbffc..68bc0933 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -31,6 +31,26 @@ class Auth { } } + async initTokenSecret() { + if (process.env.TOKEN_SECRET) { // User can supply their own token secret + Logger.debug(`[Auth] Setting token secret - using user passed in TOKEN_SECRET env var`) + this.db.serverSettings.tokenSecret = process.env.TOKEN_SECRET + } else { + Logger.debug(`[Auth] Setting token secret - using random bytes`) + this.db.serverSettings.tokenSecret = require('crypto').randomBytes(256).toString('base64') + } + await this.db.updateServerSettings() + + // New token secret creation added in v2.1.0 so generate new API tokens for each user + if (this.db.users.length) { + for (const user of this.db.users) { + user.token = await this.generateAccessToken({ userId: user.id, username: user.username }) + Logger.warn(`[Auth] User ${user.username} api token has been updated using new token secret`) + } + await this.db.updateEntities('user', this.db.users) + } + } + async authMiddleware(req, res, next) { var token = null @@ -74,7 +94,7 @@ class Auth { } generateAccessToken(payload) { - return jwt.sign(payload, process.env.TOKEN_SECRET); + return jwt.sign(payload, global.ServerSettings.tokenSecret); } authenticateUser(token) { @@ -83,12 +103,12 @@ class Auth { verifyToken(token) { return new Promise((resolve) => { - jwt.verify(token, process.env.TOKEN_SECRET, (err, payload) => { + jwt.verify(token, global.ServerSettings.tokenSecret, (err, payload) => { if (!payload || err) { Logger.error('JWT Verify Token Failed', err) return resolve(null) } - var user = this.users.find(u => u.id === payload.userId) + var user = this.users.find(u => u.id === payload.userId && u.username === payload.username) resolve(user || null) }) }) @@ -98,7 +118,7 @@ class Auth { return { user: user.toJSONForBrowser(), userDefaultLibraryId: user.getDefaultLibraryId(this.db.libraries), - serverSettings: this.db.serverSettings.toJSON(), + serverSettings: this.db.serverSettings.toJSONForBrowser(), Source: global.Source } } diff --git a/server/Server.js b/server/Server.js index 0d75eaea..b80a6942 100644 --- a/server/Server.js +++ b/server/Server.js @@ -136,6 +136,11 @@ class Server { await this.db.init() } + // Create token secret if does not exist (Added v2.1.0) + if (!this.db.serverSettings.tokenSecret) { + await this.auth.initTokenSecret() + } + await this.checkUserMediaProgress() // Remove invalid user item progress await this.purgeMetadata() // Remove metadata folders without library item await this.cacheManager.ensureCachePaths() @@ -314,7 +319,7 @@ class Server { const newRoot = req.body.newRoot let rootPash = newRoot.password ? await this.auth.hashPass(newRoot.password) : '' if (!rootPash) Logger.warn(`[Server] Creating root user with no password`) - let rootToken = await this.auth.generateAccessToken({ userId: 'root' }) + let rootToken = await this.auth.generateAccessToken({ userId: 'root', username: newRoot.username }) await this.db.createRootUser(newRoot.username, rootPash, rootToken) res.sendStatus(200) @@ -459,8 +464,6 @@ class Server { await this.db.updateEntity('user', user) const initialPayload = { - // TODO: this is sent with user auth now, update mobile app to use that then remove this - serverSettings: this.db.serverSettings.toJSON(), metadataPath: global.MetadataPath, configPath: global.ConfigPath, user: client.user.toJSONForBrowser(), diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 399ce742..c760b7da 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -242,7 +242,7 @@ class MiscController { const userResponse = { user: req.user, userDefaultLibraryId: req.user.getDefaultLibraryId(this.db.libraries), - serverSettings: this.db.serverSettings.toJSON(), + serverSettings: this.db.serverSettings.toJSONForBrowser(), Source: global.Source } res.json(userResponse) diff --git a/server/controllers/UserController.js b/server/controllers/UserController.js index f8b49638..6e2f4b9b 100644 --- a/server/controllers/UserController.js +++ b/server/controllers/UserController.js @@ -43,7 +43,7 @@ class UserController { account.id = getId('usr') account.pash = await this.auth.hashPass(account.password) delete account.password - account.token = await this.auth.generateAccessToken({ userId: account.id }) + account.token = await this.auth.generateAccessToken({ userId: account.id, username }) account.createdAt = Date.now() var newUser = new User(account) var success = await this.db.insertEntity('user', newUser) @@ -74,12 +74,14 @@ class UserController { } var account = req.body + var shouldUpdateToken = false if (account.username !== undefined && account.username !== user.username) { var usernameExists = this.db.users.find(u => u.username.toLowerCase() === account.username.toLowerCase()) if (usernameExists) { return res.status(500).send('Username already taken') } + shouldUpdateToken = true } // Updating password @@ -90,6 +92,10 @@ class UserController { var hasUpdated = user.update(account) if (hasUpdated) { + if (shouldUpdateToken) { + user.token = await this.auth.generateAccessToken({ userId: user.id, username: user.username }) + Logger.info(`[UserController] User ${user.username} was generated a new api token`) + } await this.db.updateEntity('user', user) } diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 4860d494..ddb8a086 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -5,6 +5,7 @@ const Logger = require('../../Logger') class ServerSettings { constructor(settings) { this.id = 'server-settings' + this.tokenSecret = null // Scanner this.scannerParseSubtitle = false @@ -63,6 +64,7 @@ class ServerSettings { } construct(settings) { + this.tokenSecret = settings.tokenSecret this.scannerFindCovers = !!settings.scannerFindCovers this.scannerCoverProvider = settings.scannerCoverProvider || 'google' this.scannerParseSubtitle = settings.scannerParseSubtitle @@ -110,9 +112,10 @@ class ServerSettings { } } - toJSON() { + toJSON() { // Use toJSONForBrowser if sending to client return { id: this.id, + tokenSecret: this.tokenSecret, // Do not return to client scannerFindCovers: this.scannerFindCovers, scannerCoverProvider: this.scannerCoverProvider, scannerParseSubtitle: this.scannerParseSubtitle, @@ -145,6 +148,12 @@ class ServerSettings { } } + toJSONForBrowser() { + const json = this.toJSON() + delete json.tokenSecret + return json + } + update(payload) { var hasUpdates = false for (const key in payload) {