Update:JWT signing

This commit is contained in:
advplyr 2022-07-18 17:19:16 -05:00
parent 86ee4dcff2
commit 9e7b84f289
9 changed files with 76 additions and 24 deletions

View File

@ -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,

View File

@ -13,11 +13,12 @@
<widgets-online-indicator :value="!!userOnline" />
<h1 class="text-xl pl-2">{{ username }}</h1>
</div>
<div class="cursor-pointer text-gray-400 hover:text-white" @click="copyToClipboard(userToken)">
<p v-if="userToken" class="py-2 text-xs">
<strong class="text-white">API Token: </strong><br /><span class="text-white">{{ userToken }}</span
><span class="material-icons pl-2 text-base">content_copy</span>
</p>
<div v-if="userToken" class="flex text-xs mt-4">
<ui-text-input-with-label label="API Token" :value="userToken" readonly />
<div class="px-1 mt-8 cursor-pointer" @click="copyToClipboard(userToken)">
<span class="material-icons pl-2 text-base">content_copy</span>
</div>
</div>
<div class="w-full h-px bg-white bg-opacity-10 my-2" />
<div class="py-2">
@ -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 []

View File

@ -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) {

View File

@ -1,4 +1,3 @@
if (process.env.TOKEN_SECRET == null) process.env.TOKEN_SECRET = '09f26e402586e2faa8da4c98a35f1b20d6b033c6097befa8be3486a829587fe2f90a832bd3ff9d42710a4da095a2ce285b009f0c3730cd9b8e1af3eb84df6611'
const server = require('./server/Server')
global.appRoot = __dirname

View File

@ -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
}
}

View File

@ -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(),

View File

@ -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)

View File

@ -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)
}

View File

@ -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) {