mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-07-13 10:55:05 +02:00
Update jwt secret handling
This commit is contained in:
parent
d0d152c20d
commit
8775e55762
6 changed files with 39 additions and 40 deletions
|
@ -63,13 +63,6 @@ class Auth {
|
||||||
return passport.authenticate('jwt', { session: false })(req, res, next)
|
return passport.authenticate('jwt', { session: false })(req, res, next)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a token which is used to encrpt/protect the jwts.
|
|
||||||
*/
|
|
||||||
async initTokenSecret() {
|
|
||||||
return this.tokenManager.initTokenSecret()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to generate a jwt token for a given user
|
* Function to generate a jwt token for a given user
|
||||||
* TODO: Old method with no expiration
|
* TODO: Old method with no expiration
|
||||||
|
@ -132,7 +125,7 @@ class Auth {
|
||||||
new JwtStrategy(
|
new JwtStrategy(
|
||||||
{
|
{
|
||||||
jwtFromRequest: ExtractJwt.fromExtractors([ExtractJwt.fromAuthHeaderAsBearerToken(), ExtractJwt.fromUrlQueryParameter('token')]),
|
jwtFromRequest: ExtractJwt.fromExtractors([ExtractJwt.fromAuthHeaderAsBearerToken(), ExtractJwt.fromUrlQueryParameter('token')]),
|
||||||
secretOrKey: Database.serverSettings.tokenSecret,
|
secretOrKey: TokenManager.TokenSecret,
|
||||||
// Handle expiration manaully in order to disable api keys that are expired
|
// Handle expiration manaully in order to disable api keys that are expired
|
||||||
ignoreExpiration: true
|
ignoreExpiration: true
|
||||||
},
|
},
|
||||||
|
|
|
@ -156,14 +156,11 @@ class Server {
|
||||||
}
|
}
|
||||||
|
|
||||||
await Database.init(false)
|
await Database.init(false)
|
||||||
|
// Create or set JWT secret in token manager
|
||||||
|
await this.auth.tokenManager.initTokenSecret()
|
||||||
|
|
||||||
await Logger.logManager.init()
|
await Logger.logManager.init()
|
||||||
|
|
||||||
// Create token secret if does not exist (Added v2.1.0)
|
|
||||||
if (!Database.serverSettings.tokenSecret) {
|
|
||||||
await this.auth.initTokenSecret()
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.cleanUserData() // Remove invalid user item progress
|
await this.cleanUserData() // Remove invalid user item progress
|
||||||
await CacheManager.ensureCachePaths()
|
await CacheManager.ensureCachePaths()
|
||||||
|
|
||||||
|
@ -264,7 +261,7 @@ class Server {
|
||||||
// enable express-session
|
// enable express-session
|
||||||
app.use(
|
app.use(
|
||||||
expressSession({
|
expressSession({
|
||||||
secret: global.ServerSettings.tokenSecret,
|
secret: this.auth.tokenManager.TokenSecret,
|
||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: false,
|
saveUninitialized: false,
|
||||||
cookie: {
|
cookie: {
|
||||||
|
|
|
@ -7,8 +7,13 @@ const requestIp = require('../libs/requestIp')
|
||||||
const jwt = require('../libs/jsonwebtoken')
|
const jwt = require('../libs/jsonwebtoken')
|
||||||
|
|
||||||
class TokenManager {
|
class TokenManager {
|
||||||
|
/** @type {string} JWT secret key */
|
||||||
|
static TokenSecret = null
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
/** @type {number} Refresh token expiry in seconds */
|
||||||
this.RefreshTokenExpiry = parseInt(process.env.REFRESH_TOKEN_EXPIRY) || 7 * 24 * 60 * 60 // 7 days
|
this.RefreshTokenExpiry = parseInt(process.env.REFRESH_TOKEN_EXPIRY) || 7 * 24 * 60 * 60 // 7 days
|
||||||
|
/** @type {number} Access token expiry in seconds */
|
||||||
this.AccessTokenExpiry = parseInt(process.env.ACCESS_TOKEN_EXPIRY) || 12 * 60 * 60 // 12 hours
|
this.AccessTokenExpiry = parseInt(process.env.ACCESS_TOKEN_EXPIRY) || 12 * 60 * 60 // 12 hours
|
||||||
|
|
||||||
if (parseInt(process.env.REFRESH_TOKEN_EXPIRY) > 0) {
|
if (parseInt(process.env.REFRESH_TOKEN_EXPIRY) > 0) {
|
||||||
|
@ -19,28 +24,28 @@ class TokenManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get TokenSecret() {
|
||||||
|
return TokenManager.TokenSecret
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a token which is used to encrypt/protect the jwts.
|
* Token secret is used to sign and verify JWTs
|
||||||
|
* Set by ENV variable "JWT_SECRET_KEY" or generated and stored on server settings if not set
|
||||||
*/
|
*/
|
||||||
async initTokenSecret() {
|
async initTokenSecret() {
|
||||||
if (process.env.TOKEN_SECRET) {
|
if (process.env.JWT_SECRET_KEY) {
|
||||||
// User can supply their own token secret
|
// Use user supplied token secret
|
||||||
Database.serverSettings.tokenSecret = process.env.TOKEN_SECRET
|
Logger.info('[TokenManager] JWT secret key set from ENV variable')
|
||||||
|
TokenManager.TokenSecret = process.env.JWT_SECRET_KEY
|
||||||
|
} else if (!Database.serverSettings.tokenSecret) {
|
||||||
|
// Generate new token secret and store it on server settings
|
||||||
|
Logger.info('[TokenManager] JWT secret key not found, generating one')
|
||||||
|
TokenManager.TokenSecret = require('crypto').randomBytes(256).toString('base64')
|
||||||
|
Database.serverSettings.tokenSecret = TokenManager.TokenSecret
|
||||||
|
await Database.updateServerSettings()
|
||||||
} else {
|
} else {
|
||||||
Database.serverSettings.tokenSecret = require('crypto').randomBytes(256).toString('base64')
|
// Use existing token secret from server settings
|
||||||
}
|
TokenManager.TokenSecret = Database.serverSettings.tokenSecret
|
||||||
await Database.updateServerSettings()
|
|
||||||
|
|
||||||
// TODO: Old method of non-expiring tokens
|
|
||||||
// New token secret creation added in v2.1.0 so generate new API tokens for each user
|
|
||||||
const users = await Database.userModel.findAll({
|
|
||||||
attributes: ['id', 'username', 'token']
|
|
||||||
})
|
|
||||||
if (users.length) {
|
|
||||||
for (const user of users) {
|
|
||||||
user.token = this.generateAccessToken(user)
|
|
||||||
await user.save({ hooks: false })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,7 +75,7 @@ class TokenManager {
|
||||||
*/
|
*/
|
||||||
static validateAccessToken(token) {
|
static validateAccessToken(token) {
|
||||||
try {
|
try {
|
||||||
return jwt.verify(token, global.ServerSettings.tokenSecret)
|
return jwt.verify(token, TokenManager.TokenSecret)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -85,7 +90,7 @@ class TokenManager {
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
generateAccessToken(user) {
|
generateAccessToken(user) {
|
||||||
return jwt.sign({ userId: user.id, username: user.username }, global.ServerSettings.tokenSecret)
|
return jwt.sign({ userId: user.id, username: user.username }, TokenManager.TokenSecret)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -104,7 +109,7 @@ class TokenManager {
|
||||||
expiresIn: this.AccessTokenExpiry
|
expiresIn: this.AccessTokenExpiry
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return jwt.sign(payload, global.ServerSettings.tokenSecret, options)
|
return jwt.sign(payload, TokenManager.TokenSecret, options)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(`[TokenManager] Error generating access token for user ${user.id}: ${error}`)
|
Logger.error(`[TokenManager] Error generating access token for user ${user.id}: ${error}`)
|
||||||
return null
|
return null
|
||||||
|
@ -127,7 +132,7 @@ class TokenManager {
|
||||||
expiresIn: this.RefreshTokenExpiry
|
expiresIn: this.RefreshTokenExpiry
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return jwt.sign(payload, global.ServerSettings.tokenSecret, options)
|
return jwt.sign(payload, TokenManager.TokenSecret, options)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(`[TokenManager] Error generating refresh token for user ${user.id}: ${error}`)
|
Logger.error(`[TokenManager] Error generating refresh token for user ${user.id}: ${error}`)
|
||||||
return null
|
return null
|
||||||
|
@ -261,7 +266,7 @@ class TokenManager {
|
||||||
async handleRefreshToken(refreshToken, req, res) {
|
async handleRefreshToken(refreshToken, req, res) {
|
||||||
try {
|
try {
|
||||||
// Verify the refresh token
|
// Verify the refresh token
|
||||||
const decoded = jwt.verify(refreshToken, global.ServerSettings.tokenSecret)
|
const decoded = jwt.verify(refreshToken, TokenManager.TokenSecret)
|
||||||
|
|
||||||
if (decoded.type !== 'refresh') {
|
if (decoded.type !== 'refresh') {
|
||||||
Logger.error(`[TokenManager] Failed to refresh token. Invalid token type: ${decoded.type}`)
|
Logger.error(`[TokenManager] Failed to refresh token. Invalid token type: ${decoded.type}`)
|
||||||
|
|
|
@ -42,6 +42,8 @@ class ApiKeyController {
|
||||||
/**
|
/**
|
||||||
* POST: /api/api-keys
|
* POST: /api/api-keys
|
||||||
*
|
*
|
||||||
|
* @this {import('../routers/ApiRouter')}
|
||||||
|
*
|
||||||
* @param {RequestWithUser} req
|
* @param {RequestWithUser} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
|
@ -69,7 +71,7 @@ class ApiKeyController {
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyId = uuidv4() // Generate key id ahead of time to use in JWT
|
const keyId = uuidv4() // Generate key id ahead of time to use in JWT
|
||||||
const apiKey = await Database.apiKeyModel.generateApiKey(keyId, req.body.name, req.body.expiresIn)
|
const apiKey = await Database.apiKeyModel.generateApiKey(this.auth.tokenManager.TokenSecret, keyId, req.body.name, req.body.expiresIn)
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
Logger.error(`[ApiKeyController] create: Error generating API key`)
|
Logger.error(`[ApiKeyController] create: Error generating API key`)
|
||||||
|
|
|
@ -157,12 +157,13 @@ class ApiKey extends Model {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a new api key
|
* Generate a new api key
|
||||||
|
* @param {string} tokenSecret
|
||||||
* @param {string} keyId
|
* @param {string} keyId
|
||||||
* @param {string} name
|
* @param {string} name
|
||||||
* @param {number} [expiresIn] - Seconds until the api key expires or undefined for no expiration
|
* @param {number} [expiresIn] - Seconds until the api key expires or undefined for no expiration
|
||||||
* @returns {Promise<string>}
|
* @returns {Promise<string>}
|
||||||
*/
|
*/
|
||||||
static async generateApiKey(keyId, name, expiresIn) {
|
static async generateApiKey(tokenSecret, keyId, name, expiresIn) {
|
||||||
const options = {}
|
const options = {}
|
||||||
if (expiresIn && !isNaN(expiresIn) && expiresIn > 0) {
|
if (expiresIn && !isNaN(expiresIn) && expiresIn > 0) {
|
||||||
options.expiresIn = expiresIn
|
options.expiresIn = expiresIn
|
||||||
|
@ -175,7 +176,7 @@ class ApiKey extends Model {
|
||||||
name,
|
name,
|
||||||
type: 'api'
|
type: 'api'
|
||||||
},
|
},
|
||||||
global.ServerSettings.tokenSecret,
|
tokenSecret,
|
||||||
options,
|
options,
|
||||||
(err, token) => {
|
(err, token) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|
|
@ -7,6 +7,7 @@ const User = require('../../models/User')
|
||||||
class ServerSettings {
|
class ServerSettings {
|
||||||
constructor(settings) {
|
constructor(settings) {
|
||||||
this.id = 'server-settings'
|
this.id = 'server-settings'
|
||||||
|
/** @type {string} JWT secret key ONLY used when JWT_SECRET_KEY is not set in ENV */
|
||||||
this.tokenSecret = null
|
this.tokenSecret = null
|
||||||
|
|
||||||
// Scanner
|
// Scanner
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue