Update jwt secret handling
Some checks are pending
Run Component Tests / Run Component Tests (push) Waiting to run
Integration Test / build and test (push) Waiting to run
Run Unit Tests / Run Unit Tests (push) Waiting to run

This commit is contained in:
advplyr 2025-07-08 16:39:50 -05:00
parent d0d152c20d
commit 8775e55762
6 changed files with 39 additions and 40 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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