mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-11-13 20:33:35 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d89e3a58c | ||
|
|
41cbcf551d | ||
|
|
1e60d7af64 | ||
|
|
92bae242f1 | ||
|
|
34b037087b | ||
|
|
844b04c6ae | ||
|
|
c38fe60360 | ||
|
|
8c87ae8bad | ||
|
|
f85d03ce26 |
No files matched your search
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.37.2",
|
||||
"version": "3.0.0-beta",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.37.2",
|
||||
"version": "3.0.0-beta",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@nuxtjs/axios": "^5.13.6",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.37.2",
|
||||
"version": "3.0.0-beta",
|
||||
"buildNumber": 1,
|
||||
"description": "Self-hosted audiobook and podcast client",
|
||||
"main": "index.js",
|
||||
|
||||
@@ -13,11 +13,6 @@
|
||||
<widgets-online-indicator :value="!!userOnline" />
|
||||
<h1 class="text-xl pl-2">{{ username }}</h1>
|
||||
</div>
|
||||
<div v-if="legacyToken" class="text-xs space-y-2 mt-4">
|
||||
<ui-text-input-with-label label="Legacy API Token" :value="legacyToken" readonly show-copy />
|
||||
|
||||
<p class="text-warning" v-html="$strings.MessageAuthenticationLegacyTokenWarning" />
|
||||
</div>
|
||||
<div class="w-full h-px bg-white/10 my-2" />
|
||||
<div class="py-2">
|
||||
<h1 class="text-lg mb-2 text-white/90 px-2 sm:px-0">{{ $strings.HeaderListeningStats }}</h1>
|
||||
@@ -102,12 +97,6 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
legacyToken() {
|
||||
return this.user.token
|
||||
},
|
||||
userToken() {
|
||||
return this.user.accessToken
|
||||
},
|
||||
bookCoverAspectRatio() {
|
||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||
},
|
||||
|
||||
+1
-24
@@ -40,15 +40,6 @@
|
||||
|
||||
<p v-if="error" class="text-error text-center py-2">{{ error }}</p>
|
||||
|
||||
<div v-if="showNewAuthSystemMessage" class="mb-4">
|
||||
<widgets-alert type="warning">
|
||||
<div>
|
||||
<p>{{ $strings.MessageAuthenticationSecurityMessage }}</p>
|
||||
<a v-if="showNewAuthSystemAdminMessage" href="https://github.com/advplyr/audiobookshelf/discussions/4460" target="_blank" class="underline">{{ $strings.LabelMoreInfo }}</a>
|
||||
</div>
|
||||
</widgets-alert>
|
||||
</div>
|
||||
|
||||
<form v-show="login_local" @submit.prevent="submitForm">
|
||||
<label class="text-xs text-gray-300 uppercase">{{ $strings.LabelUsername }}</label>
|
||||
<ui-text-input v-model.trim="username" autocomplete="username" :disabled="processing" class="mb-3 w-full" inputName="username" />
|
||||
@@ -94,10 +85,7 @@ export default {
|
||||
MetadataPath: '',
|
||||
login_local: true,
|
||||
login_openid: false,
|
||||
authFormData: null,
|
||||
// New JWT auth system re-login flags
|
||||
showNewAuthSystemMessage: false,
|
||||
showNewAuthSystemAdminMessage: false
|
||||
authFormData: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -201,8 +189,6 @@ export default {
|
||||
},
|
||||
async submitForm() {
|
||||
this.error = null
|
||||
this.showNewAuthSystemMessage = false
|
||||
this.showNewAuthSystemAdminMessage = false
|
||||
this.processing = true
|
||||
|
||||
const payload = {
|
||||
@@ -238,15 +224,6 @@ export default {
|
||||
}
|
||||
})
|
||||
.then((res) => {
|
||||
// Force re-login if user is using an old token with no expiration
|
||||
if (res.user.isOldToken) {
|
||||
this.username = res.user.username
|
||||
this.showNewAuthSystemMessage = true
|
||||
// Admin user sees link to github discussion
|
||||
this.showNewAuthSystemAdminMessage = res.user.type === 'admin' || res.user.type === 'root'
|
||||
return false
|
||||
}
|
||||
|
||||
this.setUser(res)
|
||||
return true
|
||||
})
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.37.2",
|
||||
"version": "3.0.0-beta",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.37.2",
|
||||
"version": "3.0.0-beta",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.37.2",
|
||||
"version": "3.0.0-beta",
|
||||
"buildNumber": 1,
|
||||
"description": "Self-hosted audiobook and podcast server",
|
||||
"main": "dist-server/index.js",
|
||||
|
||||
+2
-16
@@ -62,18 +62,6 @@ class Auth {
|
||||
return passport.authenticate('jwt', { session: false })(req, res, next)
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to generate a jwt token for a given user
|
||||
* TODO: Old method with no expiration
|
||||
* @deprecated
|
||||
*
|
||||
* @param {{ id:string, username:string }} user
|
||||
* @returns {string}
|
||||
*/
|
||||
generateAccessToken(user) {
|
||||
return this.tokenManager.generateAccessToken(user)
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate all JWT sessions for a given user
|
||||
* If user is current user and refresh token is valid, rotate tokens for the current session
|
||||
@@ -269,10 +257,8 @@ class Auth {
|
||||
} else {
|
||||
// UI request -> check if we have a callback url
|
||||
if (req.cookies.auth_cb) {
|
||||
let stateQuery = req.cookies.auth_state ? `&state=${req.cookies.auth_state}` : ''
|
||||
// UI request -> redirect to auth_cb url and send the jwt token as parameter
|
||||
// TODO: Temporarily continue sending the old token as setToken
|
||||
res.redirect(302, `${req.cookies.auth_cb}?setToken=${userResponse.user.token}&accessToken=${userResponse.user.accessToken}${stateQuery}`)
|
||||
const stateQuery = req.cookies.auth_state ? `&state=${req.cookies.auth_state}` : ''
|
||||
res.redirect(302, `${req.cookies.auth_cb}?accessToken=${userResponse.user.accessToken}${stateQuery}`)
|
||||
} else {
|
||||
Logger.error('[Auth] No callback or already expired')
|
||||
res.redirect(`/login?error=${encodeURIComponent('No callback or already expired')}&autoLaunch=0`)
|
||||
|
||||
+2
-3
@@ -415,10 +415,9 @@ class Database {
|
||||
* Create root user
|
||||
* @param {string} username
|
||||
* @param {string} pash
|
||||
* @param {Auth} auth
|
||||
* @returns {Promise<boolean>} true if created
|
||||
*/
|
||||
async createRootUser(username, pash, auth) {
|
||||
async createRootUser(username, pash) {
|
||||
if (!this.sequelize) return false
|
||||
const transaction = await this.sequelize.transaction()
|
||||
try {
|
||||
@@ -427,7 +426,7 @@ class Database {
|
||||
await transaction.rollback()
|
||||
return false
|
||||
}
|
||||
await this.userModel.createRootUser(username, pash, auth, transaction)
|
||||
await this.userModel.createRootUser(username, pash, transaction)
|
||||
await transaction.commit()
|
||||
this.hasRootUser = true
|
||||
return true
|
||||
|
||||
+1
-1
@@ -479,7 +479,7 @@ class Server {
|
||||
const rootUsername = newRoot.username || 'root'
|
||||
const rootPash = newRoot.password ? await this.auth.localAuthStrategy.hashPassword(newRoot.password) : ''
|
||||
if (!rootPash) Logger.warn(`[Server] Creating root user with no password`)
|
||||
const created = await Database.createRootUser(rootUsername, rootPash, this.auth)
|
||||
const created = await Database.createRootUser(rootUsername, rootPash)
|
||||
if (!created) {
|
||||
Logger.error(`[Server] attempt to init server when server already has a root user`)
|
||||
return res.sendStatus(500)
|
||||
|
||||
@@ -80,7 +80,9 @@ class TokenManager {
|
||||
*/
|
||||
static isBearerAccessTokenPayload(decoded) {
|
||||
if (!decoded?.userId) return false
|
||||
if (decoded.type === 'refresh') return false
|
||||
// Legacy API tokens were signed with only userId and username, and never expired.
|
||||
if (decoded.type !== 'access') return false
|
||||
if (!decoded.exp) return false
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -100,30 +102,6 @@ class TokenManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a JWT token for a given user
|
||||
* TODO: Old method with no expiration
|
||||
* @deprecated
|
||||
*
|
||||
* @param {{ id:string, username:string }} user
|
||||
* @returns {string}
|
||||
*/
|
||||
static generateAccessToken(user) {
|
||||
return jwt.sign({ userId: user.id, username: user.username }, TokenManager.TokenSecret)
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to generate a jwt token for a given user
|
||||
* TODO: Old method with no expiration
|
||||
* @deprecated
|
||||
*
|
||||
* @param {{ id:string, username:string }} user
|
||||
* @returns {string}
|
||||
*/
|
||||
generateAccessToken(user) {
|
||||
return TokenManager.generateAccessToken(user)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate access token for a given user
|
||||
*
|
||||
@@ -325,15 +303,6 @@ class TokenManager {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Temporary flag to report old tokens to users
|
||||
// May be a better place for this but here means we dont have to decode the token again
|
||||
if (!jwt_payload.exp && !user.isOldToken) {
|
||||
Logger.debug(`[TokenManager] User ${user.username} is using an access token without an expiration`)
|
||||
user.isOldToken = true
|
||||
} else if (jwt_payload.exp && user.isOldToken !== undefined) {
|
||||
delete user.isOldToken
|
||||
}
|
||||
|
||||
// approve login
|
||||
done(null, user)
|
||||
}
|
||||
|
||||
@@ -28,13 +28,12 @@ class UserController {
|
||||
*/
|
||||
async findAll(req, res) {
|
||||
if (!req.user.isAdminOrUp) return res.sendStatus(403)
|
||||
const hideRootToken = !req.user.isRoot
|
||||
|
||||
const includes = (req.query.include || '').split(',').map((i) => i.trim())
|
||||
|
||||
// Minimal toJSONForBrowser does not include mediaProgress and bookmarks
|
||||
const allUsers = await Database.userModel.findAll()
|
||||
const users = allUsers.map((u) => u.toOldJSONForBrowser(hideRootToken, true))
|
||||
const users = allUsers.map((u) => u.toOldJSONForBrowser(true))
|
||||
|
||||
if (includes.includes('latestSession')) {
|
||||
for (const user of users) {
|
||||
@@ -97,7 +96,7 @@ class UserController {
|
||||
return oldMediaProgress
|
||||
})
|
||||
|
||||
const userJson = req.reqUser.toOldJSONForBrowser(!req.user.isRoot)
|
||||
const userJson = req.reqUser.toOldJSONForBrowser()
|
||||
|
||||
userJson.mediaProgress = oldMediaProgresses
|
||||
|
||||
@@ -128,7 +127,6 @@ class UserController {
|
||||
|
||||
const userId = uuidv4()
|
||||
const pash = await this.auth.localAuthStrategy.hashPassword(req.body.password)
|
||||
const token = this.auth.generateAccessToken({ id: userId, username: req.body.username })
|
||||
const userType = req.body.type || 'user'
|
||||
|
||||
// librariesAccessible and itemTagsSelected can be on req.body or req.body.permissions
|
||||
@@ -173,7 +171,6 @@ class UserController {
|
||||
username: req.body.username,
|
||||
email: typeof req.body.email === 'string' ? req.body.email : null,
|
||||
pash,
|
||||
token,
|
||||
isActive: !!req.body.isActive,
|
||||
permissions,
|
||||
bookmarks: [],
|
||||
@@ -236,16 +233,13 @@ class UserController {
|
||||
}
|
||||
|
||||
let hasUpdates = false
|
||||
let shouldUpdateToken = false
|
||||
let shouldInvalidateJwtSessions = false
|
||||
// When changing username create a new API token
|
||||
if (updatePayload.username && updatePayload.username !== user.username) {
|
||||
const usernameExists = await Database.userModel.checkUserExistsWithUsername(updatePayload.username)
|
||||
if (usernameExists) {
|
||||
return res.status(400).send('Username already taken')
|
||||
}
|
||||
user.username = updatePayload.username
|
||||
shouldUpdateToken = true
|
||||
shouldInvalidateJwtSessions = true
|
||||
hasUpdates = true
|
||||
}
|
||||
@@ -327,11 +321,6 @@ class UserController {
|
||||
}
|
||||
|
||||
if (hasUpdates) {
|
||||
if (shouldUpdateToken) {
|
||||
user.token = this.auth.generateAccessToken(user)
|
||||
Logger.info(`[UserController] User ${user.username} has generated a new api token`)
|
||||
}
|
||||
|
||||
// Handle JWT session invalidation for username/password changes
|
||||
if (shouldInvalidateJwtSessions) {
|
||||
const newTokens = await this.auth.invalidateJwtSessionsForUser(user, req, res)
|
||||
|
||||
+3
-18
@@ -5,7 +5,6 @@ const { LRUCache } = require('lru-cache')
|
||||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const { isNullOrNaN } = require('../utils')
|
||||
const TokenManager = require('../auth/TokenManager')
|
||||
|
||||
class UserCache {
|
||||
constructor() {
|
||||
@@ -94,8 +93,6 @@ class User extends Model {
|
||||
this.pash
|
||||
/** @type {string} */
|
||||
this.type
|
||||
/** @type {string} */
|
||||
this.token
|
||||
/** @type {boolean} */
|
||||
this.isActive
|
||||
/** @type {boolean} */
|
||||
@@ -186,20 +183,16 @@ class User extends Model {
|
||||
* Create root user
|
||||
* @param {string} username
|
||||
* @param {string} pash
|
||||
* @param {import('../Auth')} auth
|
||||
* @returns {Promise<User>}
|
||||
*/
|
||||
static async createRootUser(username, pash, auth, transaction = null) {
|
||||
static async createRootUser(username, pash, transaction = null) {
|
||||
const userId = uuidv4()
|
||||
|
||||
const token = auth.generateAccessToken({ id: userId, username })
|
||||
|
||||
const newUser = {
|
||||
id: userId,
|
||||
type: 'root',
|
||||
username,
|
||||
pash,
|
||||
token,
|
||||
isActive: true,
|
||||
permissions: this.getDefaultPermissionsForUserType('root'),
|
||||
bookmarks: [],
|
||||
@@ -314,15 +307,12 @@ class User extends Model {
|
||||
const username = userinfo.preferred_username || userinfo.name || userinfo.sub
|
||||
const email = userinfo.email && userinfo.email_verified ? userinfo.email : null
|
||||
|
||||
const token = TokenManager.generateAccessToken({ id: userId, username })
|
||||
|
||||
const newUser = {
|
||||
id: userId,
|
||||
type: 'user',
|
||||
username,
|
||||
email,
|
||||
pash: null,
|
||||
token,
|
||||
isActive: true,
|
||||
permissions: this.getDefaultPermissionsForUserType('user'),
|
||||
bookmarks: [],
|
||||
@@ -514,6 +504,7 @@ class User extends Model {
|
||||
email: DataTypes.STRING,
|
||||
pash: DataTypes.STRING,
|
||||
type: DataTypes.STRING,
|
||||
// Column remains so existing databases do not need a migration. New users leave it null.
|
||||
token: DataTypes.STRING,
|
||||
isActive: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
@@ -596,11 +587,10 @@ class User extends Model {
|
||||
/**
|
||||
* User data for browser using old model
|
||||
*
|
||||
* @param {boolean} [hideRootToken=false]
|
||||
* @param {boolean} [minimal=false]
|
||||
* @returns
|
||||
*/
|
||||
toOldJSONForBrowser(hideRootToken = false, minimal = false) {
|
||||
toOldJSONForBrowser(minimal = false) {
|
||||
const seriesHideFromContinueListening = this.extraData?.seriesHideFromContinueListening || []
|
||||
const librariesAccessible = this.permissions?.librariesAccessible || []
|
||||
const itemTagsSelected = this.permissions?.itemTagsSelected || []
|
||||
@@ -613,11 +603,6 @@ class User extends Model {
|
||||
username: this.username,
|
||||
email: this.email,
|
||||
type: this.type,
|
||||
// TODO: Old non-expiring token
|
||||
token: this.type === 'root' && hideRootToken ? '' : this.token,
|
||||
// TODO: Temporary flag not saved in db that is set in Auth.js jwtAuthCheck
|
||||
// Necessary to detect apps using old tokens that no longer match the old token stored on the user
|
||||
isOldToken: this.isOldToken,
|
||||
mediaProgress: this.mediaProgresses?.map((mp) => mp.getOldMediaProgress()) || [],
|
||||
seriesHideFromContinueListening: [...seriesHideFromContinueListening],
|
||||
bookmarks: this.bookmarks?.map((b) => ({ ...b })) || [],
|
||||
|
||||
@@ -75,9 +75,6 @@ class Stream extends EventEmitter {
|
||||
get codecsToForceAAC() {
|
||||
return ['alac', 'ac3', 'eac3', 'opus']
|
||||
}
|
||||
get userToken() {
|
||||
return this.user.token
|
||||
}
|
||||
// Fmp4 does not work on iOS devices: https://github.com/advplyr/audiobookshelf-app/issues/85
|
||||
// Workaround: Force AAC transcode for FLAC
|
||||
get hlsSegmentType() {
|
||||
|
||||
@@ -39,6 +39,16 @@ describe('TokenManager', () => {
|
||||
describe('jwtAuthCheck', () => {
|
||||
const user = { id: userId, username: 'testuser', isActive: true }
|
||||
|
||||
it('rejects legacy API tokens that have no type or expiration', async () => {
|
||||
const legacyToken = jwt.sign({ userId, username: 'testuser' }, secret)
|
||||
const decoded = jwt.verify(legacyToken, secret)
|
||||
const done = sinon.spy()
|
||||
|
||||
await tokenManager.jwtAuthCheck(decoded, done)
|
||||
|
||||
expect(done.calledWith(null, null)).to.be.true
|
||||
})
|
||||
|
||||
it('rejects refresh tokens for API auth', async () => {
|
||||
const refreshToken = tokenManager.generateRefreshToken(user)
|
||||
const decoded = jwt.verify(refreshToken, secret)
|
||||
|
||||
Reference in new issue
Block a user