diff --git a/server/Auth.js b/server/Auth.js index 1d229ceb..55eb334a 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -63,13 +63,6 @@ class Auth { 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 * TODO: Old method with no expiration @@ -132,7 +125,7 @@ class Auth { new JwtStrategy( { 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 ignoreExpiration: true }, diff --git a/server/Server.js b/server/Server.js index cf0ad5c7..99e72d0c 100644 --- a/server/Server.js +++ b/server/Server.js @@ -156,14 +156,11 @@ class Server { } await Database.init(false) + // Create or set JWT secret in token manager + await this.auth.tokenManager.initTokenSecret() 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 CacheManager.ensureCachePaths() @@ -264,7 +261,7 @@ class Server { // enable express-session app.use( expressSession({ - secret: global.ServerSettings.tokenSecret, + secret: this.auth.tokenManager.TokenSecret, resave: false, saveUninitialized: false, cookie: { diff --git a/server/auth/TokenManager.js b/server/auth/TokenManager.js index cc4783b5..3f5cc836 100644 --- a/server/auth/TokenManager.js +++ b/server/auth/TokenManager.js @@ -7,8 +7,13 @@ const requestIp = require('../libs/requestIp') const jwt = require('../libs/jsonwebtoken') class TokenManager { + /** @type {string} JWT secret key */ + static TokenSecret = null + constructor() { + /** @type {number} Refresh token expiry in seconds */ 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 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() { - if (process.env.TOKEN_SECRET) { - // User can supply their own token secret - Database.serverSettings.tokenSecret = process.env.TOKEN_SECRET + if (process.env.JWT_SECRET_KEY) { + // Use user supplied 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 { - Database.serverSettings.tokenSecret = require('crypto').randomBytes(256).toString('base64') - } - 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 }) - } + // Use existing token secret from server settings + TokenManager.TokenSecret = Database.serverSettings.tokenSecret } } @@ -70,7 +75,7 @@ class TokenManager { */ static validateAccessToken(token) { try { - return jwt.verify(token, global.ServerSettings.tokenSecret) + return jwt.verify(token, TokenManager.TokenSecret) } catch (err) { return null } @@ -85,7 +90,7 @@ class TokenManager { * @returns {string} */ 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 } try { - return jwt.sign(payload, global.ServerSettings.tokenSecret, options) + return jwt.sign(payload, TokenManager.TokenSecret, options) } catch (error) { Logger.error(`[TokenManager] Error generating access token for user ${user.id}: ${error}`) return null @@ -127,7 +132,7 @@ class TokenManager { expiresIn: this.RefreshTokenExpiry } try { - return jwt.sign(payload, global.ServerSettings.tokenSecret, options) + return jwt.sign(payload, TokenManager.TokenSecret, options) } catch (error) { Logger.error(`[TokenManager] Error generating refresh token for user ${user.id}: ${error}`) return null @@ -261,7 +266,7 @@ class TokenManager { async handleRefreshToken(refreshToken, req, res) { try { // Verify the refresh token - const decoded = jwt.verify(refreshToken, global.ServerSettings.tokenSecret) + const decoded = jwt.verify(refreshToken, TokenManager.TokenSecret) if (decoded.type !== 'refresh') { Logger.error(`[TokenManager] Failed to refresh token. Invalid token type: ${decoded.type}`) diff --git a/server/controllers/ApiKeyController.js b/server/controllers/ApiKeyController.js index f60480df..c09d0736 100644 --- a/server/controllers/ApiKeyController.js +++ b/server/controllers/ApiKeyController.js @@ -42,6 +42,8 @@ class ApiKeyController { /** * POST: /api/api-keys * + * @this {import('../routers/ApiRouter')} + * * @param {RequestWithUser} req * @param {Response} res */ @@ -69,7 +71,7 @@ class ApiKeyController { } 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) { Logger.error(`[ApiKeyController] create: Error generating API key`) diff --git a/server/models/ApiKey.js b/server/models/ApiKey.js index 7b61f731..7c61611d 100644 --- a/server/models/ApiKey.js +++ b/server/models/ApiKey.js @@ -157,12 +157,13 @@ class ApiKey extends Model { /** * Generate a new api key + * @param {string} tokenSecret * @param {string} keyId * @param {string} name * @param {number} [expiresIn] - Seconds until the api key expires or undefined for no expiration * @returns {Promise} */ - static async generateApiKey(keyId, name, expiresIn) { + static async generateApiKey(tokenSecret, keyId, name, expiresIn) { const options = {} if (expiresIn && !isNaN(expiresIn) && expiresIn > 0) { options.expiresIn = expiresIn @@ -175,7 +176,7 @@ class ApiKey extends Model { name, type: 'api' }, - global.ServerSettings.tokenSecret, + tokenSecret, options, (err, token) => { if (err) { diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 29913e44..4f0aa97b 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -7,6 +7,7 @@ const User = require('../../models/User') class ServerSettings { constructor(settings) { this.id = 'server-settings' + /** @type {string} JWT secret key ONLY used when JWT_SECRET_KEY is not set in ENV */ this.tokenSecret = null // Scanner